Python: Larkが1.0.0になったので試してみたんだけど挙動がおかしい

年が明けたら Lark の 1.0.0 がリリースされていたのに気が付いたので、以前作ったコマンド形式の入力ファイルをパースするのを、再度やってみた。フレームワーク的なパッケージにまとめて再利用できるようにしてみようと思う……んだけど、どうも挙動がおかしい。

要点を先に書くと、書いたパーサも入力ファイルも同じなのに、実行するたびに結果が異なる。原因はわからない。パーサの書き方が悪いのか、Lark のバグなのか。

順を追って説明する。

コマンド形式の入力ファイル

プログラムには何らかの入力が必要で、入力ファイルにも何らかのフォーマットが必要だ。コマンド形式というのは、入力データの内部表現を、ひとつずつコマンドを実行しながら組み立てようというものだ。この記事で扱うのは、そのフレームワーク的なライブラリで、ファイルからの入力である文字列を、コマンド(と引数)の列にパースするもの。

サンプルとして次のような入力ファイルを使う。

TITLE "example"
ADD 1 2
MUL
    3
//    5
    7  8  
    9
// this line is comment.
REPEAT "Hello!" 5  
    // this is comment too.
JOIN "Hello," "world!"  // in-line comment.
ADD-2  5  .7e03
NO-ARG-COMMAND
COMMAND-WITH-KEYWORD
    KW1  1
    KW2  2
    KW3  on
    KW4  off

文法

全体を「スクリプト」と呼ぶ。文法は次の通り。

  • 「スクリプト」は1つ以上の「ステートメント」からなる
  • 「ステートメント」は「コマンド」と0個以上の「引数」からなり、空白文字で区切られ、改行文字で終わる
  • ただし、空白文字で始まる行は前の行の続きとみなす。なので「コマンド」は必ず行頭からはじまる
  • 「引数」には数値、文字列、キーワード、真偽値の4種類がある
  • //から行末まではコメント

「引数」について補足しておく。数値は文字通りの数値だけど、整数/実数の区別はなし。文字列はダブルークォートで囲む。真偽値は true / falseyes / noon / off のいずれか(すべて小文字)。

で、キーワードが今日の主題。「引数」のリストの中で、次の引数が何のデータかを示す目印に使う。要するに Python のメソッドのキーワード引数のような使い方だ。大文字のラテン文字と数字からなり、行頭には来ないことでコマンドと区別できる。実装としては Ruby なら Symbol をつかうところだけど、Python にはそういうのがないので(ないよね?)、Keyword クラスを定義した。

文法ファイルは次のようになった。

?script : statement+

statement : line continued*

line : command _WS_INLINE arglist _WS_INLINE? _NL
     | command _WS_INLINE? _NL

continued : _INDENT arglist _WS_INLINE? _NL

command : CMDNAME

CMDNAME : UCASE_LETTER ("-"|UCASE_LETTER|DIGIT)*

arglist : arg
        | arglist _WS_INLINE arg

arg : number
    | string
    | keyword
    | boolean

number : SIGNED_NUMBER

string : ESCAPED_STRING

keyword : KWORD

KWORD : UCASE_LETTER (UCASE_LETTER|DIGIT)*

boolean : true
        | false

true : "true" | "yes" | "on"

false : "false" | "no" | "off"

_WS_INLINE : WS_INLINE

_NL : NEWLINE

_INDENT : WS_INLINE


%import common.UCASE_LETTER
%import common.DIGIT
%import common.SIGNED_NUMBER
%import common.ESCAPED_STRING
%import common.NEWLINE
%import common.WS_INLINE
%import common.CPP_COMMENT

COMMENT : WS_INLINE? CPP_COMMENT NEWLINE
COMMENT_INLINE : WS_INLINE? CPP_COMMENT

%ignore COMMENT
%ignore COMMENT_INLINE

パーサ

from lark import Lark
from lark.exceptions import UnexpectedInput
from lark.visitors import Interpreter


class Parser():
    def __init__(self):
        with open('grammer.lark', 'r') as f:
            grammer = f.read()
        self.parser = Lark(grammer, start='script')

    def parse(self, input_data):
        try:
            tree = self.parser.parse(input_data)
        except UnexpectedInput as e:
            context = e.get_context(input_data)
            print(f'Syntax error:  line = {e.line}  column = {e.column}\n')
            print(context)
            exit(1)

        script = ScriptInterpreter().visit(tree)
        return script


class ScriptInterpreter(Interpreter):
    def script(self, tree):
        return [self.visit(c) for c  in tree.children]

    def statement(self, tree):
        (cmd, args1) = self.visit(tree.children[0])
        args2 = flatten([ self.visit(a) for a in tree.children[1:] ])
        return (cmd, args1 + args2)

    def line(self, tree):
        cmd = self.visit(tree.children[0])
        if len(tree.children) > 1:
            args = flatten(self.visit(tree.children[1]))
        else:
            args = []
        return (cmd, args)

    def continued(self, tree):
        return self.visit(tree.children[0])

    def command(self, tree):
        return tree.children[0]

    def arglist(self, tree):
        args = [ self.visit(a) for a in tree.children ]
        return args

    def arg(self, tree):
        return self.visit(tree.children[0])

    def number(self, tree):
        return float(tree.children[0])

    def string(self, tree):
        return tree.children[0].strip('"')

    def keyword(self, tree):
        return Keyword(str(tree.children[0]))

    def boolean(self, tree):
        return self.visit(tree.children[0])

    def true(self, tree):
        return True

    def false(self, tree):
        return False


class Keyword():
    def __init__(self, val):
        self.val = val

    def __str__(self):
        return f'Keyword<{self.val}>'

    def __repr__(self):
        return f'Keyword<{self.val}>'


def flatten(lis):
    result = []
    for elem in lis:
        if isinstance(elem, list):
            result += flatten(elem)
        else:
            result.append(elem)
    return result

テスト用のスクリプトと実行結果

テスト用なので、入力データ(の内部表現)を組み立てる代わりにコマンドと引数リストを出力する。

from parsers import Parser
import sys


def main():
    parser = Parser()

    with open(sys.argv[1], 'r') as f:
        input_data = f.read()

    script = parser.parse(input_data)

    print('SCRIPT')
    for (cmd, args) in script:
        print('  COMMAND: ' + cmd)
        print('     ARGS: ' + repr(args))



main()

これを実行すると次のようになる。

takatoh@sofa: inputscriptparser-sample > python main.py example.dat
SCRIPT
  COMMAND: TITLE
     ARGS: ['example']
  COMMAND: ADD
     ARGS: [1.0, 2.0]
  COMMAND: MUL
     ARGS: [3.0, 7.0, 8.0, 9.0]
  COMMAND: REPEAT
     ARGS: ['Hello!', 5.0]
  COMMAND: JOIN
     ARGS: ['Hello,', 'world!']
  COMMAND: ADD-2
     ARGS: [5.0, 700.0]
  COMMAND: NO-ARG-COMMAND
     ARGS: []
  COMMAND: COMMAND-WITH-KEYWORD
     ARGS: [Keyword<KW1>, 1.0, Keyword<KW2>, 2.0, Keyword<KW3>, True, Keyword<KW4>, False]

これは期待通り。ところが、何度か実行を続けると、時々次のような結果になる。

takatoh@sofa: inputscriptparser-sample > python main.py example.dat
SCRIPT
  COMMAND: TITLE
     ARGS: ['example']
  COMMAND: ADD
     ARGS: [1.0, 2.0]
  COMMAND: MUL
     ARGS: [3.0, 7.0, 8.0, 9.0]
  COMMAND: REPEAT
     ARGS: ['Hello!', 5.0, Keyword<JOIN>, 'Hello,', 'world!']
  COMMAND: ADD-2
     ARGS: [5.0, 700.0]
  COMMAND: NO-ARG-COMMAND
     ARGS: []
  COMMAND: COMMAND-WITH-KEYWORD
     ARGS: [Keyword<KW1>, 1.0, Keyword<KW2>, 2.0, Keyword<KW3>, True, Keyword<KW4>, False]

コマンドであるべき JOIN が、REPEAT コマンドの引数リストの中に、JOIN というキーワードとして含まれてしまっている(JOIN に続く引数もろとも)。

最初に書いたとおり、文法ファイルも入力ファイルも、パーサも何も変えてない。なのに実行するたびに、JOIN だけコマンドになったりキーワードになったりする。

今のところ全くの原因不明。

気になるところといえば、入力ファイルの JOIN コマンドの前の行が空白文字とコメントであること。さらにもう一つ前の行の行末(5 のうしろ)に空白文字があること(この記事では見えないけど)だ。だけどこれは文法上は問題ないはずに思える。実際期待通りの結果になることもあるんだし。

というわけで、このままでは安心して使えない。どうしようか。

AWS CLIでAmazon S3のバケットにファイルをアップロードする

AWS CLI は AWS の各サービスをコマンドラインから使えるツールだ。Amazon S3 をデータのバックアップ用に使おうと思って試してみた。

インストールと設定

AWS の Web ページからダウンロードする。↓のページから。

ダウンロードした awscliv2.zip を解凍する。

takatoh@apostrophe:~$ unzip awscliv2.zip

できた aws ディレクトリにあるインストールスクリプトを実行。

takatoh@apostrophe:~$ sudo aws/install
You can now run: /usr/local/bin/aws --version

メッセージにあるとおりコマンド名は aws だ。バージョンを確認してみよう。

takatoh@apostrophe:~$ aws --version
aws-cli/2.4.4 Python/3.8.8 Linux/5.4.0-91-generic exe/x86_64.ubuntu.20 prompt/off

続いて設定。aws configure コマンドを実行。

takatoh@apostrophe:~$ aws configure
AWS Access Key ID [None]: XXXXXXXXXXXXXXXXXXXX
AWS Secret Access Key [None]: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Default region name [None]: us-west-2
Default output format [None]: json

AWS Access Key ID と AWS Secret Access Key はあらかじめ取得しておくこと。

S3にファイルをアップロード

aws s3 cp コマンドを利用する。今回はテスト用の panicblanket-test というバケットにファイルをアップロードする。

takatoh@apostrophe:~$ aws s3 cp sample.zip s3://panicblanket-test/sample.zip

ディレクトリごとアップロードするには aws s3 sync コマンドが便利。バックアップ用途にはこちらがいいだろう。

takatoh@apostrophe:~$ aws s3 sync sample s3://panicblanket-test/sample
upload: sample/sample-1.zip to s3://panicblanket-test/sample/sample-1.zip
upload: sample/sample-2.zip to s3://panicblanket-test/sample/sample-2.zip

sample ディレクトリにあった2つのファイルがアップロードされた。

あとはシェルスクリプトを書いて定期的に事項するようにしておけば良さそうだ。

Python: 複数の文字列の共通する部分文字列をとりだす

今日は Python。何がしたいかというと、要するに ‘abcde’、’abcxz’、’ab-op’ という文字列から、共通する部分文字列(ただし先頭から) ‘ab’ をとりだしたい。

探せばいいのがあるかと思ったけど、見つからないので書いた。こんなふうになった。

import itertools

def common_substring(*strings):
    def same_all(*args):
        piv = args[0]
        return all([piv == e for e in args[1:]])
    return ''.join([s[0] for s in itertools.takewhile(lambda x: same_all(*x), zip(*strings))])

試してみよう。

>>> s1 = 'abcde'
>>> s2 = 'abcyz'
>>> s3 = 'ab-op'
>>> common_substring(s1, s2, s3)
'ab'

日本語でもいける。

>>> common_substring('こんにちは', 'こんばんは', 'こんちは')
'こん'

可変長引数をとるので、文字列は何個あってもいい。

Windows11にJDK17をインストールした

Java についてはもう何年も前から、ライセンスで有料化だとか、開発主体が Oracle から離れたとかいう話を聞いていて、部外者から見ると何がどうなってんだかわからない状態なんだけど、Java で書かなきゃいけなさそうな事案に遭遇したので調べてみた。

といっても、ググっても情報が新旧入り混じっててむしろ混乱した。

なんとなくわかったことは:

  • Java SE 17 が最新
  • コミュニティによって OpenJDK として開発されてて、Oracle はじめいろんなところからバイナリ配布されてるらしい
  • 以前はランタイムだけの JRE ってのもあったけど今はない。開発ツールを含んだ JDK だけ

くらいなとこか。

結局 Oracle の JDK に msi インストーラがあったのでこれをインストールした。

Windows 用のインストーラなので指示通りにクリックしていけばOK。試しにコンソールから実行してみるとこんな感じ。

takatoh@sofa: Documents > javac -version
javac 17.0.1
takatoh@sofa: Documents > java -version
java version "17.0.1" 2021-10-19 LTS
Java(TM) SE Runtime Environment (build 17.0.1+12-LTS-39)
Java HotSpot(TM) 64-Bit Server VM (build 17.0.1+12-LTS-39, mixed mode, sharing)

まぁ、何とかなるだろ。

Python: ローカルネットワー上のプライベートなPyPIリポジトリからpoetry addする on Windows

一昨日、Windows 10 上に pypiserver で立てたプライベートリポジトリから pip コマンドでパッケージをインストールできたので、今日は Poetry が使えるか試してみた。

まずは pypi-server コマンドでサーバを起動してプライベートリポジトリを作っておく。

takatoh@montana: Documents > pypi-server -p 8080 ./py-packages

別のターミナルを開いて、Poetry で新しいプロジェクトを作って中に移動する。

takatoh@montana: Documents > poetry new py-sample
Created package py_sample in py-sample
takatoh@montana: Documents > cd py-sample

Poetry にプライベートリポジトリを登録する。

takatoh@montana: py-sample > poetry config repositories.local http://localhost:8080/simple/

リポジトリの名前が local で、その URL が http://localhost:8080/simple/ だ。

つづいて pyproject.toml ファイルにプライベートリポジトリを参照するように次を追記する。

[[tool.poetry.source]]
name = "local"
url = "http://localhost:8080/simple/"

これで準備は完了。poetry install や poetry add でパッケージをインストールできるはず。あと、キャッシュを削除しておくのを忘れずに(まだ直ってない)。今回は PyPI.org には公開してない、自作の outputdatareader っていうパッケージを追加してみた。

takatoh@montana: py-sample > poetry add outputdatareader
Using version ^0.1.0 for outputdatareader

Updating dependencies
Resolving dependencies...

Writing lock file

Package operations: 11 installs, 0 updates, 0 removals

  • Installing pyparsing (2.4.7)
  • Installing atomicwrites (1.4.0)
  • Installing attrs (21.2.0)
  • Installing colorama (0.4.4)
  • Installing more-itertools (8.10.0)
  • Installing packaging (21.0)
  • Installing pluggy (0.13.1)
  • Installing py (1.10.0)
  • Installing wcwidth (0.2.5)
  • Installing outputdatareader (0.1.0)
  • Installing pytest (5.4.3)

無事インストールできた。

ちなみに、プライベートリポジトリには outputdatareader パッケージしか置いてないから、ほかのパッケージは PyPI.org からインストールしてる。

サーバ側のターミナルにはこんなふうに出力されている。

127.0.0.1 - - [22/Oct/2021 22:41:31] "GET /simple/outputdatareader/ HTTP/1.1" 200 353
127.0.0.1 - - [22/Oct/2021 22:41:33] "GET /simple/outputdatareader/ HTTP/1.1" 200 353
127.0.0.1 - - [22/Oct/2021 22:41:36] "GET /simple/pyparsing/ HTTP/1.1" 303 0
127.0.0.1 - - [22/Oct/2021 22:41:42] "GET /simple/atomicwrites/ HTTP/1.1" 303 0
127.0.0.1 - - [22/Oct/2021 22:41:42] "GET /simple/packaging/ HTTP/1.1" 303 0
127.0.0.1 - - [22/Oct/2021 22:41:42] "GET /simple/more-itertools/ HTTP/1.1" 303 0
127.0.0.1 - - [22/Oct/2021 22:41:42] "GET /simple/py/ HTTP/1.1" 303 0
127.0.0.1 - - [22/Oct/2021 22:41:42] "GET /simple/attrs/ HTTP/1.1" 303 0
127.0.0.1 - - [22/Oct/2021 22:41:42] "GET /simple/pluggy/ HTTP/1.1" 303 0
127.0.0.1 - - [22/Oct/2021 22:41:42] "GET /simple/colorama/ HTTP/1.1" 303 0
127.0.0.1 - - [22/Oct/2021 22:41:42] "GET /simple/wcwidth/ HTTP/1.1" 303 0
127.0.0.1 - - [22/Oct/2021 22:41:48] "GET /simple/outputdatareader/ HTTP/1.1" 200 353
127.0.0.1 - - [22/Oct/2021 22:41:48] "GET /simple/pytest/ HTTP/1.1" 303 0
127.0.0.1 - - [22/Oct/2021 22:41:50] "GET /packages/outputdatareader-0.1.0-py3-none-any.whl HTTP/1.1" 200 2016

outputdatareader 以外のパッケージではステータスコードが 303 (See Other)になってる。つまりプライベートリポジトリには無いってことで、そうすると Poetry は PyPI.org に探しに行く。最後の行から、プライベートリポジトリからは outputdatareader-0.1.0-py3-none-any.whl ファイルだけがダウンロードされてるのがわかる。

というわけで、今日はここまで。

SQLite3のデータベースファイルからSQLにダンプする

たいしたネタじゃないけど、ときどき必要になる割にいつも調べてる気がするのでメモしておく。

SQLite3 のデータベースは1つのファイルになってる。このデータベースの中身を SQL にダンプするには sqlite3 コマンドをつかって、次の2通りのやり方がある。

対話的インターフェイスでダンプする方法

データベースを開くと、コマンド入力待ち状態になる。

takatoh@wplj:db$ sqlite3 production.sqlite3
SQLite version 3.11.0 2016-02-15 17:29:24
Enter ".help" for usage hints.
sqlite>

ここで .dump コマンドでダンプすることができるけど、このままだと画面に出力されてしまうので、先に .output コマンドで出力先をファイルに変更しておく。

sqlite> .output ./dump.sql

これでカレントディレクトリの dump.sql ファイルに出力されるようになる。そしたら .dump コマンド。

sqlite> .dump

引数なしだとデータベース内のすべてのテーブルをダンプする。特定のテーブルだけをダンプするには、そのテーブル名を引数に与えればいい。

終わったら .exit

sqlite> .exit

コマンドラインから直接ダンプする方法

パイプを使って sqlite3 コマンドにダンプを指示する .dump コマンドを流し込んでやればいい。デフォルトでは標準出力に書き出すので、ファイルにリダイレクトする。

takatoh@wplj:db$ echo '.dump' | sqlite3 production.sqlite3 > dump2.sql

定期的にバックアップするような場合にはこっちのほうが便利。

Python: ローカルネットワーク上のプライベートなPyPIリポジトリからインストールする on Windows

先月同じようなタイトルの記事を書いたけど……

pip コマンドの --find-links オプションを使えば、パッケージファイルを放り込んだディレクトリを指定するだけでインストールすることができた。のだけれど、poetry を使うときにはこの方法ではダメだった。結局のところ PyPI.org 相当のプレイべートなリポジトリサーバを立ててやる必要がある。

選択肢としては pypiserver っていうパッケージがあって、それは知ってたんだけど、設定とか面倒そうだと勝手に思ったので今までやってみなかった。ところが今日試してみたら存外に簡単にできたのでメモしておくことにする。記事のタイトルにもあるように環境は Windows。

pypiserver 自体は pip コマンドでインストールできる。

takatoh@montana: Documents > pip install pypiserver
Looking in links: http://nightschool/py-packages/
Collecting pypiserver
  Using cached pypiserver-1.4.2-py2.py3-none-any.whl (77 kB)
Installing collected packages: pypiserver
Successfully installed pypiserver-1.4.2

これで完了。以下のようにインストールされてるのが確認できる。

takatoh@montana: Documents > pip list | grep pypiserver
pypiserver                        1.4.2

つぎに、パッケージファイルを置いておくフォルダを作る。

takatoh@montana: Documents > mkdir py-packages

    Directory: C:\Users\takatoh\Documents

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d----          2021/10/19    20:32                py-packages

で、ここに自作のパッケージファイルを放り込んだ。

takatoh@montana: Documents > ls py-packages

    Directory: C:\Users\takatoh\Documents\py-packages

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a---          2021/10/17    18:51           4035 brs-0.8.1-py3-none-any.whl

あとは pypi-server コマンドで起動すれば準備は完了。

takatoh@montana: Documents > pypi-server -p 8080 ./py-packages

-p 8080 オプションでポートを、引数でパッケージファイルの入っているフォルダを指定している。

さて、別のコンソールを立ち上げて pip install してみる。--extra-index-url http://localhost:8080/simple/ オプションでローカルホストに立てた pypiserver の URL を指定している。

takatoh@montana: takatoh > pip install --extra-index-url http://localhost:8080/simple/ brs
Looking in indexes: https://pypi.org/simple, http://localhost:8080/simple/
Looking in links: http://nightschool/py-packages/
Collecting brs
  Downloading http://localhost:8080/packages/brs-0.8.1-py3-none-any.whl (4.0 kB)
Requirement already satisfied: click<9.0.0,>=8.0.1 in c:\users\takatoh\appdata\local\programs\python\python39\lib\site-packages (from brs) (8.0.1)
Requirement already satisfied: PyYAML<6.0.0,>=5.4.1 in c:\users\takatoh\appdata\local\programs\python\python39\lib\site-packages (from brs) (5.4.1)
Requirement already satisfied: requests<3.0.0,>=2.26.0 in c:\users\takatoh\appdata\local\programs\python\python39\lib\site-packages (from brs) (2.26.0)
Requirement already satisfied: colorama in c:\users\takatoh\appdata\local\programs\python\python39\lib\site-packages (from click<9.0.0,>=8.0.1->brs) (0.4.4)
Requirement already satisfied: idna<4,>=2.5 in c:\users\takatoh\appdata\local\programs\python\python39\lib\site-packages (from requests<3.0.0,>=2.26.0->brs) (3.2)
Requirement already satisfied: certifi>=2017.4.17 in c:\users\takatoh\appdata\local\programs\python\python39\lib\site-packages (from requests<3.0.0,>=2.26.0->brs) (2021.5.30)
Requirement already satisfied: urllib3<1.27,>=1.21.1 in c:\users\takatoh\appdata\local\programs\python\python39\lib\site-packages (from requests<3.0.0,>=2.26.0->brs) (1.26.7)
Requirement already satisfied: charset-normalizer~=2.0.0 in c:\users\takatoh\appdata\local\programs\python\python39\lib\site-packages (from requests<3.0.0,>=2.26.0->brs) (2.0.6)
Installing collected packages: brs
Successfully installed brs-0.8.1

Looking in indexes: で始まる行で、PyPI.org とローカルに立てた pypiserver を見に行ってるのがわかる。つづく Looking in links: で始まる行では、先日の記事で設定したパッケージファイルの置いてある場所を探しに行っている。結果として、ローカルの pypiserver からダウンロードしてインストールしてるのがわかる。

サーバ側にはこんなふうに出力されていた。

127.0.0.1 - - [19/Oct/2021 20:35:27] "GET /simple/ HTTP/1.1" 200 207
127.0.0.1 - - [19/Oct/2021 20:35:27] "GET /favicon.ico HTTP/1.1" 404 711
127.0.0.1 - - [19/Oct/2021 20:36:42] "GET /simple/brs/ HTTP/1.1" 200 301
127.0.0.1 - - [19/Oct/2021 20:36:45] "GET /packages/brs-0.8.1-py3-none-any.whl HTTP/1.1" 200 4035

というわけで、Windows でもローカルに pypiserver を立てるのは簡単だということが分かった。

あとはこれをサービスとしてバックグラウンドで動かせれば、poetry からも使えるようになる(はず)。それはまた今度。

Python for Windows: インターネット接続を制限された環境ではpoetryがつかえない

pipはつかえる

このあいだ、インターネット接続を制限された職場のネット環境でもpipを使う方法を見つけた、という記事を書いた。少し時間が開いてしまったけど、今日試してみた。結果は「pipはつかえる」だ。

検証のために、インストールしてあった Python をいったん削除(キャッシュやらなんやらも全部削除)して、インストールしなおした。まぁ、バージョンアップもかねて、ね。

username@workpc: Documents > python -V
Python 3.9.7

この状態ではパッケージは2つしかインストールされてない。

username@workpc: Documents > pip list
Package    Version
---------- -------
pip        21.2.3
setuptools 57.4.0

で、PyPI.org にアクセスせずローカルなネットワークで完結するよう、pip の設定ファイル %APPDATA%\Roming\pip\pip.ini を書いておく。

[global]
find-links = file://workpc/py-packages

[install]
find-links = file://workpc/py-packages
no-index = yes

[list]
no-index = yes

このあいだの記事に書いたように、--find-links オプションを指定する代わりにここに書いておく。ここでは file://workpc/py-packages を指定している。no-index = yes は PyPI.org を見に行かないようにする設定。これがないと繋がらない PyPI.org にアクセスしようとして時間を無駄にすることになる。

つぎは上の find-links で指定した共有フォルダを作る。といっても自分の PC にテキトーなフォルダを作って py-packages って名前で共有するだけだ。あとはあらかじめダウンロードしておいたパッケージファイルを放り込んでおく。

さて、これで準備は完了。試しに poetry をインストールしてみた。

username@workpc: Documents > pip install poetry
Looking in links: file://workpc/py-packages
Processing \\workpc\py-packages\poetry-1.1.11-py2.py3-none-any.whl
Processing \\workpc\py-packages\html5lib-1.1-py2.py3-none-any.whl
Processing \\workpc\py-packages\keyring-21.8.0-py3-none-any.whl
Processing \\workpc\py-packages\shellingham-1.4.0-py2.py3-none-any.whl
Processing \\workpc\py-packages\tomlkit-0.7.2-py2.py3-none-any.whl
Processing \\workpc\py-packages\clikit-0.6.2-py2.py3-none-any.whl
Processing \\workpc\py-packages\requests_toolbelt-0.9.1-py2.py3-none-any.whl
Processing \\workpc\py-packages\cleo-0.8.1-py2.py3-none-any.whl
Processing \\workpc\py-packages\poetry_core-1.0.7-py2.py3-none-any.whl
Processing \\workpc\py-packages\cachy-0.3.0-py2.py3-none-any.whl
Processing \\workpc\py-packages\requests-2.26.0-py2.py3-none-any.whl
Processing \\workpc\py-packages\virtualenv-20.8.1-py2.py3-none-any.whl
Processing \\workpc\py-packages\crashtest-0.3.1-py3-none-any.whl
Processing \\workpc\py-packages\pexpect-4.8.0-py2.py3-none-any.whl
Processing \\workpc\py-packages\cachecontrol-0.12.6-py2.py3-none-any.whl
Processing \\workpc\py-packages\pkginfo-1.7.1-py2.py3-none-any.whl
Processing \\workpc\py-packages\packaging-20.9-py2.py3-none-any.whl
Processing \\workpc\py-packages\msgpack-1.0.2-cp39-cp39-win_amd64.whl
Processing \\workpc\py-packages\lockfile-0.12.2-py2.py3-none-any.whl
Processing \\workpc\py-packages\pylev-1.4.0-py2.py3-none-any.whl
Processing \\workpc\py-packages\pastel-0.2.1-py2.py3-none-any.whl
Processing \\workpc\py-packages\webencodings-0.5.1-py2.py3-none-any.whl
Processing \\workpc\py-packages\six-1.16.0-py2.py3-none-any.whl
Processing \\workpc\py-packages\pywin32_ctypes-0.2.0-py2.py3-none-any.whl
Processing \\workpc\py-packages\pyparsing-2.4.7-py2.py3-none-any.whl
Processing \\workpc\py-packages\ptyprocess-0.7.0-py2.py3-none-any.whl
Processing \\workpc\py-packages\urllib3-1.26.7-py2.py3-none-any.whl
Processing \\workpc\py-packages\idna-3.2-py3-none-any.whl
Processing \\workpc\py-packages\charset_normalizer-2.0.6-py3-none-any.whl
Processing \\workpc\py-packages\certifi-2021.5.30-py2.py3-none-any.whl
Processing \\workpc\py-packages\filelock-3.3.0-py3-none-any.whl
Processing \\workpc\py-packages\backports.entry_points_selectable-1.1.0-py2.py3-none-any.whl
Processing \\workpc\py-packages\platformdirs-2.4.0-py3-none-any.whl
Processing \\workpc\py-packages\distlib-0.3.3-py2.py3-none-any.whl
Installing collected packages: urllib3, idna, charset-normalizer, certifi, requests, pylev, pastel, msgpack, crashtest, webencodings, six, pywin32-ctypes, pyparsing, ptyprocess, platformdirs, lockfile, filelock, distlib, clikit, cachecontrol, backports.entry-points-selectable, virtualenv, tomlkit, shellingham, requests-toolbelt, poetry-core, pkginfo, pexpect, packaging, keyring, html5lib, cleo, cachy, poetry
Successfully installed backports.entry-points-selectable-1.1.0 cachecontrol-0.12.6 cachy-0.3.0 certifi-2021.5.30 charset-normalizer-2.0.6 cleo-0.8.1 clikit-0.6.2 crashtest-0.3.1 distlib-0.3.3 filelock-3.3.0 html5lib-1.1 idna-3.2 keyring-21.8.0 lockfile-0.12.2 msgpack-1.0.2 packaging-20.9 pastel-0.2.1 pexpect-4.8.0 pkginfo-1.7.1 platformdirs-2.4.0 poetry-1.1.11 poetry-core-1.0.7 ptyprocess-0.7.0 pylev-1.4.0 pyparsing-2.4.7 pywin32-ctypes-0.2.0 requests-2.26.0 requests-toolbelt-0.9.1 shellingham-1.4.0 six-1.16.0 tomlkit-0.7.2 urllib3-1.26.7 virtualenv-20.8.1 webencodings-0.5.1

無事完了。ちゃんとインストールできてるのも確認できる。

username@workpc: Documents > pip list
Package                           Version
--------------------------------- ---------
backports.entry-points-selectable 1.1.0
CacheControl                      0.12.6
cachy                             0.3.0
certifi                           2021.5.30
charset-normalizer                2.0.6
cleo                              0.8.1
clikit                            0.6.2
crashtest                         0.3.1
distlib                           0.3.3
filelock                          3.3.0
html5lib                          1.1
idna                              3.2
keyring                           21.8.0
lockfile                          0.12.2
msgpack                           1.0.2
packaging                         20.9
pastel                            0.2.1
pexpect                           4.8.0
pip                               21.2.3
pkginfo                           1.7.1
platformdirs                      2.4.0
poetry                            1.1.11
poetry-core                       1.0.7
ptyprocess                        0.7.0
pylev                             1.4.0
pyparsing                         2.4.7
pywin32-ctypes                    0.2.0
requests                          2.26.0
requests-toolbelt                 0.9.1
setuptools                        57.4.0
shellingham                       1.4.0
six                               1.16.0
tomlkit                           0.7.2
urllib3                           1.26.7
virtualenv                        20.8.1
webencodings                      0.5.1

というわけで、PyPI.org に繋がらない環境でも pip がつかえるのが確認できた。まぁわかってたけどね。

poetryはつかえない

つぎはインストールしたばかりの poetry を試してみた。

username@workpc: Documents > poetry config virtualenvs.in-project true
username@workpc: Documents > poetry new py-sample
Created package py_sample in py-sample

できたプロジェクトフォルダに入って、click を追加してみた。すると:

username@workpc: Documents > cd py-sample
username@workpc: py-sample > poetry add click
Creating virtualenv py-sample in C:\Users\username\Documents\py-sample\.venv

  ConnectionError

  HTTPSConnectionPool(host='pypi.org', port=443): Max retries exceeded with url: /pypi/click/json (Caused by NewConnectionError('<urllib3.connection.HTTPSConnection object at 0x0000020637A223D0>: Failed to establish a new connection: [WinError 10060] 接続済みの呼び出し 先が一定の時間を過ぎても正しく応答しなかったため、接続できませんでした。または接続済みのホストが応答しなかったため、確立された接続は失敗しました。'))

  at ~\AppData\Local\Programs\Python\Python39\lib\site-packages\requests\adapters.py:516 in send
      512│             if isinstance(e.reason, _SSLError):
      513│                 # This branch is for urllib3 v1.22 and later.
      514│                 raise SSLError(e, request=request)
      515│
    → 516│             raise ConnectionError(e, request=request)
      517│
      518│         except ClosedPoolError as e:
      519│             raise ConnectionError(e, request=request)
      520│

こんなエラーに。そうすんなりとは行ってくれないか。

ググってみると、Poetry の日本語ドキュメントのページを見つけた。これによると pyproject.toml ファイルを編集してプライベートなリポジトリを追加できるようなので、こんなふうに追記した。

[[tool.poetry.source]]
name = "private"
url = "file://workpc/py-packages/"

さっきの共有フォルダを見に行ってくれることを期待して書いたんだけど、結果は次の通り:

username@workpc: py-sample > poetry add click

  InvalidSchema

  No connection adapters were found for 'file://workpc/py-packages/click/'

  at ~\AppData\Local\Programs\Python\Python39\lib\site-packages\requests\sessions.py:742 in get_adapter
      738│             if url.lower().startswith(prefix.lower()):
      739│                 return adapter
      740│
      741│         # Nothing matches :-/
    → 742│         raise InvalidSchema("No connection adapters were found for {!r}".format(url))
      743│
      744│     def close(self):
      745│         """Closes all adapters and as such the session"""
      746│         for v in self.adapters.values():

違うエラーを吐いた。どうやら PyPI.org の代わりに指定したプライベートなリポジトリを探しに行ってはくれてるようだけど、file: スキーマをサポートしてないようだ。ドキュメントを見る限りでは、プライベートなリポジトリとはいっても PyPI.org 相当の機能が必要で、パッケージファイルが置いてあればいいというわけではないらしい。ということは pipiserver を立てるしかないのか。

残念な結果に終わった。

Python for Windows: poetryの挙動がおかしい

最近 Python をバージョンアップしたら、poetry の挙動がおかしい。poetry addpoetry install を実行して仮想環境にパッケージをインストールしようとしてもエラーを吐く。正確に言うとバージョンアップのせいなのかどうかはよくわからないのだけど、ほかに思い当たることもないんだ。

順を追ってみてみよう。Python と poetry のバージョンから。どちらも最新版。

takatoh@montana: w > python -V
Python 3.9.7
takatoh@montana: w > poetry -V
Poetry version 1.1.10

さて、まずは poetry new で新しいプロジェクトを作る。

takatoh@montana: w > poetry new py-sample
Created package py_sample in py-sample

で、できたプロジェクトのフォルダに入って poetry add する。今回はよく使う click を追加してみた。

takatoh@montana: w > cd py-sample
takatoh@montana: py-sample > poetry add click
Creating virtualenv py-sample in C:\Users\takatoh\Documents\w\py-sample\.venv
Using version ^8.0.1 for click

Updating dependencies
Resolving dependencies...

Writing lock file

Package operations: 11 installs, 0 updates, 0 removals

  • Installing pyparsing (2.4.7)
  • Installing atomicwrites (1.4.0)
  • Installing attrs (21.2.0)
  • Installing colorama (0.4.4)
  • Installing more-itertools (8.10.0)
  • Installing packaging (21.0)
  • Installing pluggy (0.13.1)
  • Installing py (1.10.0)
  • Installing wcwidth (0.2.5)
  • Installing click (8.0.1)
  • Installing pytest (5.4.3)

click のほかに仮想環境で使うパッケージもあわせて正常にインストールできた。

つぎに、いったん上のフォルダに移動して、別の新しいプロジェクトを作る。

takatoh@montana: py-sample > cd ..
takatoh@montana: w > poetry new py-sample-2
Created package py_sample_2 in py-sample-2

今できたほうのフォルダに入って、さっきと同じように poetry add で click を追加してみる。すると:

takatoh@montana: w > cd py-sample-2
takatoh@montana: py-sample-2 > poetry add click
Creating virtualenv py-sample-2 in C:\Users\takatoh\Documents\w\py-sample-2\.venv
Using version ^8.0.1 for click

Updating dependencies
Resolving dependencies...

Writing lock file

Package operations: 11 installs, 0 updates, 0 removals

  • Installing pyparsing (2.4.7)

  ValueError

  File \C:\Users\takatoh\AppData\Local\pypoetry\Cache\artifacts\92\0f\cf\effdcd5d76a6186df0969f85b3b030284ff8058936d5016540b5258ea3\pyparsing-2.4.7-py2.py3-none-any.whl does not exist

  at ~\AppData\Local\Programs\Python\Python39\lib\site-packages\poetry\core\packages\file_dependency.py:40 in __init__
       36│             except FileNotFoundError:
       37│                 raise ValueError("Directory {} does not exist".format(self._path))
       38│
       39│         if not self._full_path.exists():
    →  40│             raise ValueError("File {} does not exist".format(self._path))
       41│
       42│         if self._full_path.is_dir():
       43│             raise ValueError("{} is a directory, expected a file".format(self._path))
       44│


Failed to add packages, reverting the pyproject.toml file to its original content.

こんなふうにエラーになる。追加しようとした click じゃなくて仮想環境に必要な pyparsing っていうパッケージをインストールしようとしたところでエラーになってるのがわかる。poetry は一度インストールに使ったファイルはキャッシュしてて、このエラーはキャッシュしてるはずのファイル(\C:\Users\takatoh\AppData\Local\pypoetry\Cache\artifacts\92\0f\cf\effdcd5d76a6186df0969f85b3b030284ff8058936d5016540b5258ea3\pyparsing-2.4.7-py2.py3-none-any.whl)が見つからない、と言っている(ValueError の後に続く部分)。

ところが、ファイルはちゃんと存在する。

takatoh@montana: w > tree C:\Users\takatoh\AppData\Local\pypoetry\Cache\artifacts /F
フォルダー パスの一覧
ボリューム シリアル番号は 00000062 681C:8AA1 です
C:\USERS\TAKATOH\APPDATA\LOCAL\PYPOETRY\CACHE\ARTIFACTS
├─29
│  └─58
│      └─fc
│          └─ed8b7451d3ef91a6465024f5656141da996e7aafd4d41a1659629a75e7
│                  pluggy-0.13.1-py2.py3-none-any.whl
│
├─3d
│  └─af
│      └─11
│          └─94d692f8e0bf0791d76d42d8d257be6c7d2cba22ca4f745856c83286e9
│                  more_itertools-8.10.0-py3-none-any.whl
│
├─60
│  └─79
│      └─0b
│          └─c48bd9c2a989aa8b1eb7a67cd02b053c10734f2e4e5665f7995f09999c
│                  py-1.10.0-py2.py3-none-any.whl
│
├─6f
│  └─a9
│      └─ee
│          └─569c37f69a8c365ee41d2340aeac0214ee8c0086b8d8db43a21545204b
│                  attrs-21.2.0-py2.py3-none-any.whl
│
├─7d
│  └─f4
│      └─60
│          └─0737157bb9711fec72c70dff523aa54491eef317e0d586cf5388ff0908
│                  wcwidth-0.2.5-py2.py3-none-any.whl
│
├─92
│  └─0f
│      └─cf
│          └─effdcd5d76a6186df0969f85b3b030284ff8058936d5016540b5258ea3
│                  pyparsing-2.4.7-py2.py3-none-any.whl
│
├─9e
│  └─b3
│      └─11
│          └─7d87ac44fdb2d557301f1f4086a37c080d1482a98751abe7cdbabbad26
│                  colorama-0.4.4-py2.py3-none-any.whl
│
├─ae
│  └─32
│      └─83
│          └─e159324c1bd58177322f4e45f598d500fe22544bff20f53f55cf749da8
│                  click-8.0.1-py3-none-any.whl
│
├─b7
│  └─99
│      └─9c
│          └─c8ddc18c8225c740fc2ae6d503da3f93d5dddaf04afac3da460afacbc6
│                  atomicwrites-1.4.0-py2.py3-none-any.whl
│
├─e4
│  └─88
│      └─dd
│          └─3dbff42e3c4462dc3d027f5024025571166cb35edee215ce93852a968c
│                  pytest-5.4.3-py3-none-any.whl
│
└─f9
    └─4f
        └─09
            └─c91a145b26102e014fd6e33bd8c7b87306c8e1d4a771158f34dd13210e
                    packaging-21.0-py3-none-any.whl

pyparsing はなかほどに出力されてるね。なのに見つからないってどういうことさ。

さて、poetry の挙動がおかしいのはこれだけじゃない。というかこれから本番というか。

存在するのに見つからないっていうキャッシュをフォルダごと削除してやると、今度はインストールがうまくいく。まず削除。

takatoh@montana: w > rm C:\Users\takatoh\AppData\Local\pypoetry\Cache\artifacts -Recurse -Force

これで削除できてる。

takatoh@montana: w > tree C:\Users\takatoh\AppData\Local\pypoetry\Cache\artifacts /F
フォルダー パスの一覧
ボリューム シリアル番号は 00000066 681C:8AA1 です
C:\USERS\TAKATOH\APPDATA\LOCAL\PYPOETRY\CACHE\ARTIFACTS
無効なパスです - \USERS\TAKATOH\APPDATA\LOCAL\PYPOETRY\CACHE\ARTIFACTS
サブフォルダーは存在しません

で、あらためて poetry add click

takatoh@montana: w > cd py-sample-2
takatoh@montana: py-sample-2 > poetry add click
Using version ^8.0.1 for click

Updating dependencies
Resolving dependencies...

Package operations: 11 installs, 0 updates, 0 removals

  • Installing pyparsing (2.4.7)
  • Installing atomicwrites (1.4.0)
  • Installing attrs (21.2.0)
  • Installing colorama (0.4.4)
  • Installing more-itertools (8.10.0)
  • Installing packaging (21.0)
  • Installing pluggy (0.13.1)
  • Installing py (1.10.0)
  • Installing wcwidth (0.2.5)
  • Installing click (8.0.1)
  • Installing pytest (5.4.3)

ほら、ちゃんとインストールできた。どういうことだよ!

Python: pipでローカルネットワー上のプライベートなリポジトリからインストールする

仕事で使うちょっとしたツール(大抵はテキスト処理のツール)を Python で書いたりしてるんだけど、一つ不便なことがある。職場のネットワークから PyPI にアクセスできないんだ。

まぁ、セキュリティだの何だの考えるとインターネットへのアクセスを制限するのもわからないではない。

幸いブラウザでは繋がるので、ファイルを一つずつダウンロードしてからインストールすることは可能で、今までそうやってたんだけど不便なことこの上ない。パッケージがファイル一つで済むならまだいいんだけど、他のパッケージに依存してたりすると、正直やる気が失せてくるし、同僚に使ってもらおうというときにもハードルが高い。

さて、今日になって pip install コマンドの --find-links オプションでパッケージ置き場の URL を指定できることを知った。pypiserver みたいな PyPI クローンのサーバじゃなくて、ディレクトリにパッケージファイルを置いておくだけでいい。

早速、自宅の HTTP サーバにディレクトリを公開して試してみた。こんな感じ。

takatoh@apostrophe:~$ pip install filtre --find-links http://nightschool/py-packages/ --trusted-host nightschool
Looking in links: http://nightschool/py-packages/
Collecting filtre
  Downloading http://nightschool/py-packages/filtre-0.1.0-py3-none-any.whl (1.9 kB)
Installing collected packages: filtre
Successfully installed filtre-0.1.0

http://nightschool/py-packages/ がパッケージファイルを置いてあるディレクトリの URL。--trusted-host nightschool オプションを指定してるのは HTTPS じゃなくて HTTP だから。filtre ってのは自作のパッケージね。

さらに、Windows なら file: スキーマでも行ける。上のディレクトリは Samba で共有フォルダにしてあって file://nightschool/shared/py-packages/ でアクセスできるので、こうすればいい。

takatoh@montana: Documents > pip install filtre --find-links file://nightschool/shared/py-packages/ --user
Looking in links: file://nightschool/shared/py-packages/
Processing \\nightschool\shared\py-packages\filtre-0.1.0-py3-none-any.whl
Installing collected packages: filtre
Successfully installed filtre-0.1.0

やぁ、これでだいぶ楽になるぞ。

ちなみに、パッケージファイルをダウンロード(インストールじゃなくて)するには pip download コマンドを使えば依存パッケージも含めてダウンロードしてくれる。使いそうなパッケージを自宅でダウンロードしたら、職場に持っていってまとめて適当な共有フォルダに放り込んでおけばいい。

[追記]

pip install コマンドの --find-links オプションや --trusted-host オプションは、pip の設定ファイルに書いておけばいちいち指定しなくてもいい。こんな感じ。

[install]
find-links = http://nightschool/py-packages/
trusted-host = nightschool

pip の設定ファイルは、Linux なら ~/.conf/pip/pip.conf、Windows なら %APPDATA%\pip\pip.ini。