2020/01/06

Python Tips: Poetry の tips あれこれ



あけましておめでとうございます。

ここで言う Poetry とは Python のパッケージ管理ツールのことです。


Poetry はアプリケーション開発とパッケージ開発のどちらでも利用できるものですが、私は現状アプリケーション開発のみで Poetry を使っています。そのため、以下にあげるのはすべてアプリケーション開発で Poetry を利用するときの tips です。

この記事では Poetry のバージョン 1.0.0 が対象です。 Poetry はわりと安定している(=使い方がコロコロと変わりづらい)ツールですが、バージョンが上がるとやり方が変わる可能性があるので参考にされる際はご注意ください。

  • venv のファイルをプロジェクトディレクトリの下に置きたい
  • venv を作らずグローバルにパッケージをインストールしたい
  • アップデート可能なパッケージをチェックしたい
  • Poetry 自体をアップデートしたい
  • Docker コンテナでインストールしたい
  • どんなサブコマンドがあるのか忘れた
  • サブコマンドのオプションにどんなものがあるか忘れた
  • サブコマンドをタブ補完してほしい

venv のファイルをプロジェクトディレクトリの下に置きたい


→ 設定値 virtualenvs.in-project の値を true に変更します。

すべてのプロジェクトに適用する場合は次のとおりにします。

poetry config virtualenvs.in-project true

カレントプロジェクトにのみ適用する場合は --local オプションを付けます。

poetry config --local virtualenvs.in-project true

設定した値を初期状態にリセットしたいときは poetry config コマンドの --unset オプションを利用します。

グローバルな設定を削除:

poetry config --unset virtualenvs.in-project

プロジェクトローカルな設定を削除:

poetry config --local --unset virtualenvs.in-project

ちなみに、デフォルトでは Poetry は venv のファイルをホームディレクトリ以下の特定のディレクトリに集約して置くようになっています(私の手元の macOS 上で確認すると /Users/USERNAME/Library/Caches/pypoetry/virtualenvs でした)。

参考:

virtualenvs.in-project: boolean

Create the virtualenv inside the project's root directory. Defaults to false.


venv を作らずグローバルにパッケージをインストールしたい


→ 設定値 virtualenvs.create の値を false にします。

poetry config virtualenvs.create false

この設定が有用な場面はかぎられますが、このオプションの存在を覚えておくといざというときに便利です。

参考:

virtualenvs.create: boolean

Create a new virtual environment if one doesn't already exist. Defaults to true.


アップデート可能なパッケージをチェックしたい


poetry show コマンドの --outdated オプションを使用します。

poetry show --outdated

実際のアップデート処理を仮実行して確認したい場合は poetry update コマンドの --dry-run オプションが使えます。

poetry update --dry-run

参考:


Poetry 自体をアップデートしたい


poetry self update サブコマンドを使用します。

poetry self update

Poetry のバージョン 1.0.0 未満の頃は self:update というサブコマンドでしたが、変更されたようです。

万が一うまく行かない場合は公式の手順に従っていったんアンインストールしてからインストールし直すと解決する可能性があります。

Docker コンテナでインストールしたい


→ 単純に公式の手順に従ってインストールします。

公式の python:3.8 イメージの上にインストールする場合のサンプルは次のとおりです。

Dockerfile:



どんなサブコマンドがあるのか忘れた


→ サブコマンド無しで単に poetry と打つか help サブコマンドを使用します。

poetry

poetry help

サブコマンドのオプションにどんなものがあるか忘れた


poetry help コマンドにサブコマンド名を引数として渡します。

poetry help self

次のような内容が見やすい色付きで出力されます。

USAGE
      poetry self
  or: poetry self update [--preview] []

COMMANDS
  update
    Updates Poetry to the latest version.

                The version to update to.

    --preview            Install prereleases.

GLOBAL OPTIONS
  -h (--help)            Display this help message
  -q (--quiet)           Do not output any message
  -v (--verbose)         Increase the verbosity of messages: "-v" for normal output, "-vv" for more verbose output and "-vvv" for debug
  -V (--version)         Display this application version
  --ansi                 Force ANSI output
  --no-ansi              Disable ANSI output
  -n (--no-interaction)  Do not ask any interactive question

これを読んでもわからないことがあれば公式のドキュメントを読みます:


サブコマンドをタブ補完してほしい


poetry completions コマンドの出力を起動時のスクリプトに追加します。

公式で紹介されている macOS + Homebrew を使用している場合のサンプルは次のとおりです。

# Bash (macOS/Homebrew)
poetry completions bash > $(brew --prefix)/etc/bash_completion.d/poetry.bash-completion

参考:


古い設定値を削除したい


→ Poetry のバージョンアップによって使われなくなった設定値を削除するには、設定値が格納されたファイルを直接編集して該当行を削除します。これは poetry config --unset が効かない場合のみ使用します。

私が確認した環境( macOS / CentOS 7 )ではそのファイルは ~/.config/pypoetry/config.toml にありました。拡張子が示すとおり中身は TOML フォーマットです。

vi ~/.config/pypoetry/config.toml

以上 Poetry の tips 集でした。

2019/12/19

Python Tips: Python で FTP のアップロードを自動化したい

Python で FTP のファイルアップロード処理を行う方法についてです。

これは SSH/SCP が使えない古風なレンタルサーバーへのファイルアップロードや CMS のデプロイ等を自動化したいときに便利かと思います。

早速結論ですが、 Python で FTP を行うには ftplib という標準ライブラリを使うのがかんたんです。


ただし ftplib はじゃっかんインタフェースがあまり人間向けでないというか直感的ではありません( FTP の技術に疎い者の感想です)。

(ちなみにここでご紹介するのは ftplib の使い方のごく一部なので、以下で取り上げる使い方が各操作に対する唯一の方法というわけではありません)

早速サンプルを見てみましょう。

FTP 接続する


FTP 接続するには ftplib ライブラリのクラス FTP FTP_TLS のどちらかを使います。クラス名が示すとおり FTP は通常の FTP 用、 FTP_TLS は FTP over TLS/SSL (いわゆる FTPS )用です。

いまどき通常の FTP を使うのはナニなので、ここからは FTPS を中心に説明していきます。

FTPS 接続をするには FTP_TLS クラスにホスト名・ユーザー・パスワードを渡して使用します。 FTP_TLS は組み込み関数の open() と同じような形で、コンテキストマネージャとして使用することもできます。

FTP_TLS のインスタンスの retrlines() メソッドに特定の引数を渡して実行すると、サーバーのファイル一覧を取得することができます。

# ファイル一覧をメタ情報付きで取得する
with FTP_TLS(host='example.com', user='username', passwd='password') as ftp:
    ftp.retrlines('LIST')

# ファイル一覧をファイル名だけ取得する
with FTP_TLS(host='example.com', user='username', passwd='password') as ftp:
    ftp.retrlines('NLST')
# 標準出力に次のような出力が出る:
# home
# cgi-bin
# cgi-def
# app-def
# data
# .well-known
# log

FTP でファイルをアップロードする


ファイルをアップロードするには FTP_TLS インスタンスの storbinary() メソッドを使用します。

from ftplib import FTP_TLS


def push(remote_path: str, local_path: str) -> None:
    """FTPS でファイルを 1 件アップロードする"""
    with FTP_TLS(host='example.com', user='username', passwd='password') as ftp:
        ftp.storbinary('STOR {}'.format(remote_path), open(local_path, 'rb'))


# ローカルの `.env.production` ファイルを `.env` という名前でアップロードする
push(remote_path='.env', local_path='.env.production')

storbinary() の第 1 引数は 'STOR アップロード先のファイルパス' というフォーマットの文字列で、第 2 引数はバイナリモードで開かれた読み込み可能なファイルオブジェクトです。

ちなみに、指定されたリモートパスの親ディレクトリが存在しない場合はエラーとなります。そのため、親ディレクトリが存在するかどうかわからない深い階層にファイルをアップロードしたい場合は、事前に親ディレクトリを作成する必要があります。

ディレクトリの作成には FTP_TLS インスタンスの mkd() メソッドが使えるので、たとえば次のような関数を書くと、深い階層の場所にファイルをアップロードすることができます。

from ftplib import FTP_TLS, error_perm


def push(remote_path: str, local_path: str, create_parents: bool=False) -> None:
    """FTPS でファイルを 1 件アップロードする(親ディレクトリ作成のオプション付き)"""
    with FTP_TLS(host='example.com', user='username', passwd='password') as ftp:
        if create_parents:
            parts = remote_path.split('/')
            chain = ['/'.join(parts[:n]) for n in range(len(parts))][1:]

            for part in chain:
                try:
                    ftp.mkd('{}'.format(part))
                # ディレクトリがすでに存在する場合はエラーがあがるのでキャッチしてスルー
                except error_perm as e:
                    pass

        ftp.storbinary('STOR {}'.format(remote_path), open(local_path, 'rb'))


# ローカルの `settings/prod.py` ファイルを `data/config/settings/prod.py` というパスにアップロードする
push(remote_path='data/config/settings/prod.py', local_path='settings/prod.py')

FTP でディレクトリをまるごとアップロードする


それひとつで特定のディレクトリ以下のファイルをまるごとアップロードできるようなメソッドは(私が知るかぎり) ftplib にはありません。そのため、特定のディレクトリをまるごとアップロードしたいような場合は、事前に対象ディレクトリ以下のファイルをリストアップして、それらを 1 件ずつアップロードする、という処理が必要になります。

具体的には、次のような感じでローカルのファイルを集めてその結果を上の push() 関数に渡す、といった形になるでしょうか。

from pathlib import Path
from typing import Generator, List


def get_local_files(target: str) -> Generator[Path, None, None]:
    """特定のディレクトリ以下のファイルをすべて取得する

    - リストアップの順番は DFS (深さ優先探索)
    """
    root = Path(target)
    if not root.is_dir():
        raise Exception('Only directories can be passed.')

    edges = [root]
    while edges:
        item = edges.pop()
        children = [x for x in item.iterdir()]
        dirs = [x for x in children if x.is_dir()]
        files = [x for x in children if x.is_file()]
        edges.extend(dirs)
        yield from files


for file in get_local_files(target='.'):
    push(remote_path=str(file), local_path=str(file))

今回ご紹介したのはあくまでもイメージを伝えるためのサンプルです。 FTP は操作に誤るとクリティカルな事故につながることもあるので、くれぐれもそのまま使用しないよういしてください。参考にされる際は自己責任でお願いします。

以上です。

私はとりあえず以上のことができればやりたいこととしては十分だったのでこれ以上は調べませんでしたが、冒頭に述べたとおり、ここでご説明したのは ftplib のごく一部なので、興味がある方は公式のドキュメントをご覧になってみてください。


Python で FTP を行う方法についてでした。

2019/10/21

:= (ウォルラス演算子)の使い方


skeeze による Pixabay からの画像

Python の := 演算子について説明します。

:= 演算子は Python のバージョン 3.8 ( 2019 年 10 月リリース)で導入されました。該当する PEP は PEP 572 です。

呼び方


呼び方は、 := 演算子のことは walrus operator:= を含む式全体のことは assignment expressions と呼ぶのが主流のようです。

日本語では、前者はカタカナで ウォルラス演算子 、あるいは訳して セイウチ演算子 ( walrus = セイウチ )でしょうか。 assignment expression は別の言語で一般的な 代入式 がよいですかね。

PEP 572 には named expressions と呼ばれることもあると書かれています。

ウォルラス演算子という呼び名は日本ではあまり流行らなさそうな感じもしますが、本記事内ではウォルラス演算子と代入式という表現で行きます。

ウォルラス演算子とは


Python のウォルラス演算子は既存の = 演算子で作れる代入文( assignment statements )と同じく、名前(≒変数)に値をひもづけるための演算子です。

既存の = ・代入文は、別の式の中に組み込んで使うことができませんでした。ウォルラス演算子を使うことで、式の途中結果を名前にひもづける(≒代入する)ことができます。

JavaScript ・ Ruby ・ PHP 等 Python に似たスクリプト言語には以前から代入式が存在していたため、それらの言語をメインで使い Python をサブで使うような方には Python の既存の代入文( = )は融通が利かない・非直感的な感じに感じられたことと思います。

使い方


ウォルラス演算子・代入式は次のように使うことができます。

import random


def getvalue():
    if random.random() > 0.2:
        return 'OK'

    return None


if value := getvalue():
    print(value)

while value := getvalue():
    print(value)

このプログラムを実行すると OK という文字がランダムに 2 〜 7 行ほど表示されて終了します。

ウォルラス演算子が登場するまでは、この if 文と while 文のところはそれぞれ次のように書く必要がありました。

value = getvalue()
if value:
    print(value)

while True:
    value = getvalue()
    if value:
        print(value)
        continue
    break

ウォルラス演算子をうまく使うことでコードが短くわかりやすくなります。

メリット・使いどころ


ウォルラス演算子・代入式のメリットは、既存の代入文( = )では短く書けなかったコードが短く書けてコードがコンパクトになることです。

ただし、他の言語における代入式と同様に、濫用するとかえってわかりづらくなったりバグの温床になったりするので、むやみやたらと使うのではなく、使えばコードがよくなるところでだけ使うのがよいと思います。

使いどころのひとつは、 callable (関数やメソッド等)が必ず truthy な値か falsy な値を返し、 truthy な値を返す場合にだけ特定の処理を行いたいような場合 です。

while stream := getstream():
    process(stream)

if match := re.match(pattern, text):
    print(match)

PEP 572 には、標準ライブラリの rere.match()re.search() の例が紹介されています:

if match := re.search(pat, text):
    print("Found:", match.group(0))
elif match := re.search(otherpat, text):
    print("Alternate found:", match.group(0))
elif match := re.search(third, text):
    print("Fallback found:", match.group(0))

ウォルラス演算子による代入式は別の式の途中に差し込めるので、 デバッグのためにメソッドチェーンの途中の値を確認したいとき なんかにも便利かもしれません。例えば次のようなクラスがあるとします。

class A:
    def f1(self):
        ...
        return self

    def f2(self):
        ...
        return self

    def f3(self):
        ...
        return self

これを次のように利用している場合を考えます。

A().f1().f2().f3()

この途中にウォルラス演算子を差し込むことで値を取り出せます。

(f2_result := A().f1().f2()).f3()
logger.debug(f2_result)

その他、 lambda 式や三項演算(インラインの ifelse )との組み合わせでも価値を発揮してくれそうです。

留意点


ウォルラス演算子・代入式にはいくつか注意点があります。

スコープを作らない

ウォルラス演算子は新たなスコープを作るわけではありません。ウォルラス演算子で値にひもづけられた名前は、(削除しないかぎり)既存のスコープの末尾まで存在し続けます。 JavaScript の let のような挙動を期待して利用すると思わぬバグを生む可能性があります。

単独で文にすることはできない

ウォルラス演算子・代入式を既存の = ・代入文の代わりに使うことはできません。

次のコードを実行すると SyntaxError があがります。

val := 5
# => SyntaxError: invalid syntax

しかし、次のコードは問題なく通ります。

(val := 5)

次のコードも大丈夫です。

val2 = (val1 := 5)

ただし、このような使い方(=本来通常の = を使うべきところで := を使うこと)は紛らわしいので原則使わない方がよいと思います。 PEP でも非推奨と書かれています。

= と同じ挙動ではない

ウォルラス演算子・代入式の挙動は既存の = ・代入文と必ずしも同じではありません。

例えば、 = の代入では右辺が tuple の場合にそのかっこを省略することができますが、ウォルラス演算子の場合は右辺の tuple のかっこを省略することができません。かっこ無しで , 区切りで複数の値を右辺に書くと思いも寄らない挙動になります。

val1 = 3, 4
print(val1)
# => (3, 4)

(val2 := 3, 4)
print(val2)
# => 3

(val3, val4 := 3, 4)
NameError: name 'val3' is not defined

所感


いち Python ユーザーとしての個人的な感想です。

:= 演算子が Python に導入されると初めて聞いたときは「えー」と思いましたが、時間が経つうちに慣れてきて、いまはそれほど違和感を感じません。

ただし、従来の代入文と新しい代入式とで書き方のパターンが増えてしまうので、 Python らしいかというとあまり Python らしくはないと思います。しかし、代入式を使うときれいに書けそうなパターンがいくつか思いつくので、便利かそうでないかというと便利だと思います。

もう少し引いた視点から見ると、 Python はいま「最も使われているプログラミング言語のひとつ」でかつ「最もシェアが伸びているスクリプト言語」です。そのあたりの状況を自覚して(?)このあたりの仕様をちょっと緩くするという判断はとてもよい気がします。

以上です。というわけで、 Python の := 演算子についてでした。

参考