nikkie-ftnextの日記

イベントレポートや読書メモを発信

これまで出会った正規表現を集めたPythonライブラリ emi-re 0.1.0 を公開しました! #エミリー生誕祭2025

この記事は、エミリー・スチュアートちゃんのお誕生日を、怪文書ならぬ怪パッケージでお祝いするものです。

はじめに

エミリー・スチュアートさん、お誕生日おめでとうございます!
nikkieでしゅ

これまでに出会った印象深い正規表現を集めたPythonライブラリを公開しました。
エミリーちゃんの帳面リスペクトです!

目次

emi-re 0.1.0

https://pypi.org/project/emi-re/0.1.0/

0.1.0で記録されている正規表現
(uvは気になるパッケージをちょっと試したいときにも便利ですね)

% uv run --with emi-re python
Installed 1 package in 2ms
Python 3.13.0 ...
>>> import emire
>>> emire.to_snake_case("CamelCase")
'camel_case'
>>> emire.remove_spaces("Algorithm C ないしは アルゴリズム C")
'Algorithm CないしはアルゴリズムC'

開発裏話

どちらの正規表現も、過去にエントリをしたためました。

キャメルケースからスネークケースに正規表現を使って変換(Streamlitの実装読みより)

日本語テキスト中の半角スペースを正規表現を使って削除(mecab-ipadic-neologdの辞書データを作る上での正規化処理)

こういったOSSから学んだ正規表現をPythonパッケージにまとめてもいいなと半年くらい前に思いつきました。
思いついた時点で名前はエミリー(emire)に決めていました。
このたび2025年の生誕祭に合わせて0.1.0を公開した次第です。

エミリーちゃん、お誕生日おめでとう!!

関連エントリゾーンです。

2024年のときももちろんすきすきだったのですが、1年を得て想いは強まっていることに自分でも驚いています。
『野辺見れば』コミュ、本当にありがとう〜

終わりに

正規表現を使った便利な処理をまとめたライブラリ emi-re 0.1.0 を公開しました。
現在は以下の2つをサポート!

  • キャメルケースからスネークケースに変換
  • 日本語テキスト中の半角スペースを削除

そして、エミリーちゃん、お誕生日おめでとうございます!
このライブラリが今後どんな模様を描き出すのか、絵羽模様みもあってワクワクしています。

P.S. ハッチポッチ2に参戦したいnikkieは

見境なくHatchを使いました

PythonのAST(抽象構文木)をRustのプログラムからもdumpしたい!

はじめに

久石奏さん、お誕生日おめでとうございます。さいごのソリスト、胸を打つ...😭
nikkieです。

RustでPythonのASTを扱う、小さな小さな一歩の素振りをしました。

目次

PythonでASTをdumpする

標準ライブラリastを使います。
https://docs.python.org/ja/3/library/ast.html#command-line-usage

% python -m ast <<"EOF"
if True:
  pass # comment
EOF
Module(
   body=[
      If(
         test=Constant(value=True),
         body=[
            Pass()],
         orelse=[])],
   type_ignores=[])

rustpython-parser crate

https://crates.io/crates/rustpython-parser

偶然知りました。
ソースコードがあるのは、RustPython/Parserというリポジトリ

The parser is one of the core part of RustPython and Ruff project.

Ruffでも使われている(使われていた?1)ようです

Ruffのリポジトリに見つけたprint-ast

Contributingのページの「cargo dev」の項目。
https://docs.astral.sh/ruff/contributing/#cargo-dev
cargo dev print-ast <file>が紹介されています。

ここを見て、rustpython-parserを使ってもprint-ast相当のことができるのではないかと思いつきました。
crates.ioの「How to use」のコードでVecが返ってきます。
これをprint-astのように表示してみたくなったのです。

パクるために、print-astの実装を確認します。
https://github.com/astral-sh/ruff/blob/0.8.6/crates/ruff_dev/src/print_ast.rs

println!("{python_ast:#?}");

『Rustの練習帳』で見たところだ!

{:#?}を見て『Rustの練習帳』を思い出しました2。

{:?}を使うことで、構造体をデバッグ用の形式(リンク省略)で表示できます。(2.2.1)

今回は{:#?}を使って、改行とインデントを入れて出力を読みやすくしています。これは、整形表示(pretty-printing)と呼ばれています。(2.2.3)

Ruffのprint-astでやっているのは、整形表示(pretty-print)!

この機にドキュメントも確認

? formatting.

When used with the alternate format specifier #?, the output is pretty-printed.

rustpython-parserによるASTをpretty-print

% cargo init hello-ast
% cd hello-ast
% cargo add rustpython-parser

インストールされたrustpython-parserは0.4.0です。

% cargo run -q
[
    If(
        StmtIf {
            range: 0..13,
            test: Constant(
                ExprConstant {
                    range: 3..7,
                    value: Bool(
                        true,
                    ),
                    kind: None,
                },
            ),
            body: [
                Pass(
                    StmtPass {
                        range: 9..13,
                    },
                ),
            ],
            orelse: [],
        },
    ),
]

できた!!
Ruffのドキュメントのprint-astの出力例と一致するように思われます。

(なお、ファイルからの読み込みは後回しにしています)

終わりに

ごくごく短いPythonコード片をrustpython-parserでASTに変換し、それをpretty-printしました。
Pythonのastモジュールと比べると、できあがるASTは全然違うのですね。
例えばRuffはこんなASTを扱っているのか〜


  1. https://github.com/astral-sh/ruff を検索したところ、現在はCargo.tomlに依存の記載はなさそうでした。Ruffのリポジトリ内にもparserのcrateがあります
  2. 取り組んだことがあります

browser-useをGeminiで動かすまで(langchain-google-genai 2.0.8がPyPIにリリースされるまでは暫定的にリポジトリからインストール)

はじめに

自分もまだ正月いけます! nikkieです。

注目されているbrowser-use。
リポジトリのREADMEにあるQuick startをGeminiを指定して動かしました。
https://github.com/browser-use/browser-use/tree/0.1.17?tab=readme-ov-file#quick-start

目次

報告されているエラー

OpenAIのGPTでは動きますが、Geminiに変えるとエラーが報告されています

-from langchain_openai import ChatOpenAI
+from langchain_google_genai import ChatGoogleGenerativeAI
from browser_use import Agent
import asyncio

async def main():
    agent = Agent(
        task="Find a one-way flight from Bali to Oman on 12 January 2025 on Google Flights. Return me the cheapest option.",
-        llm=ChatOpenAI(model="gpt-4o"),
+        llm=ChatGoogleGenerativeAI(model="gemini-2.0-flash-exp"),
    )
    result = await agent.run()
    print(result)

asyncio.run(main())
INFO     [agent] 🚀 Starting task: Find a one-way flight from Bali to Oman on 12 January 2025 on Google Flights. Return me the cheapest option.
INFO     [agent]
📍 Step 1
ERROR    [agent] ❌ Result failed 1/5 times:
 Invalid argument provided to Gemini: 400 * GenerateContentRequest.tools[0].function_declarations[0].parameters.properties[action].items.properties[input_text].properties: should be non-empty for OBJECT type
* GenerateContentRequest.tools[0].function_declarations[0].parameters.properties[action].items.properties[extract_content].properties: should be non-empty for OBJECT type
* GenerateContentRequest.tools[0].function_declarations[0].parameters.properties[action].items.properties[scroll_down].properties: should be non-empty for OBJECT type
* GenerateContentRequest.tools[0].function_declarations[0].parameters.properties[action].items.properties[scroll_to_text].properties: should be non-empty for OBJECT type
* GenerateContentRequest.tools[0].function_declarations[0].parameters.properties[action].items.properties[go_to_url].properties: should be non-empty for OBJECT type
* GenerateContentRequest.tools[0].function_declarations[0].parameters.properties[action].items.properties[send_keys].properties: should be non-empty for OBJECT type
* GenerateContentRequest.tools[0].function_declarations[0].parameters.properties[action].items.properties[get_dropdown_options].properties: should be non-empty for OBJECT type
* GenerateContentRequest.tools[0].function_declarations[0].parameters.properties[action].items.properties[scroll_up].properties: should be non-empty for OBJECT type
* GenerateContentRequest.tools[0].function_declarations[0].parameters.properties[action].items.properties[open_tab].properties: should be non-empty for OBJECT type
* GenerateContentRequest.tools[0].function_declarations[0].parameters.properties[action].items.properties[go_back].properties: should be non-empty for OBJECT type
* GenerateContentRequest.tools[0].function_declarations[0].parameters.properties[action].items.properties[done].properties: should be non-empty for OBJECT type
* GenerateContentRequest.tools[0].function_declarations[0].parameters.properties[action].items.properties[select_dropdown_option].properties: should be non-empty for OBJECT type
* GenerateContentRequest.tools[0].function_declarations[0].parameters.properties[action].items.properties[click_element].properties: should be non-empty for OBJECT type
* GenerateContentRequest.tools[0].function_declarations[0].parameters.properties[action].items.properties[switch_tab].properties: should be non-empty for OBJECT type
* GenerateContentRequest.tools[0].function_declarations[0].parameters.properties[action].items.properties[search_google].properties: should be non-empty for OBJECT type

同様の報告

対処法

Issue #104 (上で埋め込んだもの)によるとlangchain-google-genaiの問題のようです。

  • PyPIの最新(2.0.7)は修正マージ前

つまり、記事執筆時点(2025/01/05時点)では、langchain-google-genaiをリポジトリからインストールすればbrowser-useをGeminiで動かせます。
これはlangchain-google-genaiの2.0.8以降がリリースされるまでのworkaroundとなります。

browser-use「Quick start」をGeminiで動かす

  • Python 3.11.8
  • pip install browser-use "langchain-google-genai @ git+ssh://[email protected]/langchain-ai/langchain-google.git#subdirectory=libs/genai"
  • playwright install
    • これが必要と認識したので仮想環境を使ったのですが、記事執筆の中でuvx playwright installをまずやって、inline script metadataでいいのではと気づきました)
  • 環境変数GOOGLE_API_KEY

stepの途中でエラーにならなかった試行
(デンパサル -> オマーン 片道なので、合ってないのではと思っています)

INFO     [agent] 📄 Result: The cheapest one-way flight from Bali to Oman on 12 January 2025 is to Muscat for 63,770 JPY.

browser-use自体私はまだ理解が追いついていないのですが、存外失敗するなという感想です。
日本語なのが関係しているんでしょうか?

余談:なぜOpenAIではないのか

微課金したのですが、現在Usage tier 1らしく、APIキー1が有効にならないみたいなんですよね(なんか最近変わりましたかね?)
https://platform.openai.com/settings/organization/limits

https://platform.openai.com/docs/guides/rate-limits#usage-tiers

Tier 2
$50 paid and 7+ days since first successful payment

現在は429が返ってきます
https://help.openai.com/en/articles/6891839-api-error-codes

終わりに

OpenAIのGPTの代わりにGeminiでbrowser-useをひとまず動かせました。
langchain-google-genai 2.0.7以前の問題でエラーが出ていましたが、リポジトリ上で修正はされています。
同様のIssueをいくつか見かけており、私も同様に動かせないかと思いましたが、対処いただいたことに感謝します。
もう少し戯れてみたいな〜

pydantic-settingsで作るCLI、LogfireでPydanticによるパース結果を収集できるじゃん!

はじめに

さいごのソリスト、ううう...😭😭😭(ユーフォ3期一挙放送見た) nikkieです。

むふふ、すごいことに気づいちゃいました〜。
Pydantic社が構想するLogfire、こいつはやべーぞ!

目次

pydantic-settingsでCLIが作れる!

argparseすきすきの民でしたが、オプション引数を環境変数からも指定できるサポートが魅力的すぎて、初手はpydantic-settingsを選びたくなっています

Logfire

Pydantic社がリリースした可観測性(observability)ツール。
SaaSとPythonパッケージの2つがあります。

先日のみんなのPython勉強会でも、PydanticAIとLogfireを簡単に連携できると紹介されました1。

今回pydantic-settingsでCLIを作る中で気づいちゃったんですよね。
PydanticAIと簡単に連携できるなら、pydantic-settingsとも簡単に連携できるのでは、と

pydantic-settingsによるCLIで、Pydanticによるパース結果を収集する

ドキュメントのトップページの内容をやっていきます。

logfire.pydantic.dev

SaaSのLogfireの設定

  • https://logfire.pydantic.dev/login からログイン
    • GitHubアカウントでログインしました
  • プロジェクトを作成(「New project」)2

  • プロジェクトでWrite Tokenを発行(プロジェクト作成後の指示に従えばよいが、プロジェクトの「Settings」を操作している)

ドキュメントにはlogfireパッケージに含まれるCLIを使った例(logfire auth・logfire projects use)がありますが、これは本番向けではないと知ったので、Write Tokenにしました。
https://logfire.pydantic.dev/docs/#__tabbed_2_1
この記事ではlogfireパッケージのCLIは使っていません

プロジェクトのWrite Tokenを環境変数LOGFIRE_TOKENに設定します。
この状態でPython処理系の対話モードを使って動作確認できます。
https://logfire.pydantic.dev/docs/#__tabbed_2_2 の例を対話モードで実行しています。

% uv run --with logfire python
>>> import logfire
>>> logfire.configure()
Logfire project URL: https://logfire.pydantic.dev/ftnext/...
>>> logfire.info("Hello, {name}!", name="world")
15:59:35.328 Hello, world!

プロジェクトのLiveを見ると

上記の動作確認によるspanが確認できました!3

CLIアプリケーションでLogfireを使う設定

  • logfireパッケージを依存に追加
  • Pydanticのパース結果を収集するコードを追加
import logfire

logfire.configure()
logfire.instrument_pydantic()

全体像

CLIを呼び出すだけで、コマンドラインで渡した引数のパース結果が記録されます!

% uv run --quiet cli.py position エミリー 5
01:02:02.731 Pydantic Settings validate_python
エミリーエミリーエミリーエミリーエミリー
Logfire project URL: https://logfire.pydantic.dev/ftnext/...

入力と解析結果がバッチリ記録されています!

{
    'input_data': {
        'position': {
            'string': 'エミリー',
            'number': '5',
        },
        'option': None,
    },
    'result': Settings(
        position=Position(
            string='エミリー',
            number=5,
        ),
        option=None,
    ),
    'schema_name': 'Settings',
    'success': True,
    'validation_method': 'validate_python',
}

動作環境

  • uv 0.4.27 (Homebrew 2024-10-25)
  • Python 3.13.0
  • pydantic-settings 2.7.1
    • Pydantic 2.10.4
  • Logfire 2.11.1

LogfireのIntegration

Integrationという仕組みで実現しています。
https://logfire.pydantic.dev/docs/integrations/#custom-integrations

logfire.pydantic.dev

logfire.instrument_pydantic()の第1引数recordは、以下の値を取りえます。
https://logfire.pydantic.dev/docs/reference/api/logfire/#logfire.Logfire.instrument_pydantic

  • "all"(デフォルト値)
  • "failure"
  • "metrics"
  • "off"

今回はデフォルト値の"all"で試していますが、データのバリデーションに失敗したときだけ記録が残ればいいのであれば"failure"の指定になりますね

他にも記録の指定をクラス単位にしたり、タグを付けたりもできるようでした(宿題事項)

終わりに

pydantic-settingsで作るCLIにLogfireを組み込めました。
CLIに渡した引数とPydanticによるパース結果を記録してみました("all")

2024年5月のUS PyConに行った方から「Pydantic社のブースでLogfire紹介してたよ」と教えてもらった記憶があります。
そのときは「なんでまた?」という感じだったのですが、実際に触って、Pydantic社がLogfireをリリースしたのに納得しています。
pydantic-settingsの例から、Pydanticが使われるあらゆる場面でLogfireの出番があります!4
広い市場を獲りにいったということですね(最良の開発者体験を構築というビジョンを実現するために!)


  1. 5代目LT王子襲名、おめでとうございます!
  2. プロジェクト作成ではスクショ用にプロジェクトを作りました(ログを記録するプロジェクトとは別です)
  3. 動作確認のログだけ示すために絞り込んでいます(PydanticAIが組み込まれてて、日本語でやりたいこと書いたらSQLになったぞ!)
  4. Integrationのドキュメントから、DjangoなどPydanticがデフォルトで入ってなさそうな領域でもLogfireに連携できるようでした。OpenTelemetry互換を謳っているようです。いまは未知の概念、宿題だ...

python-build-standaloneのAstral社移管に思うこと

はじめに

Astral、あんたまた管理するリポジトリ増やして!1 nikkieです。

Pythonコミュニティにとってよいニュースではあると思うのですが、(意見の偏りがあると自覚しつつも)私の目から見たAstral社にはモヤッとするポイントもあり、1本書くことにしました。

目次

python-build-standaloneがAstral社に移管されました

python-build-standaloneは、Gregory Szorc氏によるプロジェクトです。
現在注目されているuvはPython処理系を管理できます2が、これを可能にしているのがpython-build-standalone!
uvだけでなく、HatchやRyeのPython管理機能も支えています。
Thank you very much, Gregory!

Gregory氏によるメンテナンスが難しくなったそうで3、Astral社は2024年4月からリリースに加わっていたとのこと。

We've shepherded every release since April, (ç•¥)

12月に突然移管したわけではなく、移行期間を設けての動きだったようです。
コミュニティにおけるpython-build-standaloneの存在感を考えると、Astralが手を挙げて動いたという点には感謝です。

上記の移管発表ブログには4つのゴールが掲げられていました(The future of standalone Python distributions)

  1. Keep the project up-to-date with Python releases(Pythonのリリースに合わせてpython-build-standaloneを最新に保つ)
  2. Upstream changes to the CPython build system(上流のCPythonのビルドシステムに変更を反映する)
  3. Remove some of the project's existing limitations(python-build-standaloneに存在する制約を取り払う)
  4. Improve the project's build and release process(python-build-standaloneのビルドとリリースプロセスの改善)

python-build-standaloneは私も触って、Python開発環境構築のあまりの簡単さに衝撃を受けました(Gregory氏に技術力で殴られた...)。
メンテナンスが引き継がれることはPythonコミュニティにとって好ましいことだと思っています。

モヤッとポイント:管理するリポジトリ全部、要望に応え続けられる?

Astral社に感謝の思いはあるのですが、それが100%というわけではなく、私は半分くらいモヤッとしています。

Astral社はRuff、uv(、Rye)と注目度の高いツールをいくつも開発しています。
ちゃんと全部お世話し続けられるのかが、私から見ると不安です。
(私が重箱の隅をつついてるのかもしれませんが、)これまでのAstral社の行動からお世話し続けると確信できないんですよ

具体的には、uvやRuffでコミュニティの要望が高い機能が放置されているように私の目には映っています。
要望が高いにも関わらず、Astral社は実現をリードしていない(ように見える)。
これが私のモヤッとです。
現状お世話しきれていないのに、新しい管理対象を増やしたんだ〜と思っちゃうんですよね。

いくつか例を出しましょう。

たとえばuvにはタスクランナー機能が要望されています。
私もuvで管理するプロジェクトにuvxを並べたMakefileを毎回作っているので、これはめちゃほしいです。

ところが
https://github.com/astral-sh/uv/issues/5903#issuecomment-2277047047

Yeah we plan to support something like this! We haven't spent time on the design yet.

(意訳)このようなものをサポートすることを計画しています。まだ設計に時間を費やしていません

このコメントは2024年8月。
このIssueはたまに見るくらいなのですが、2025年年始時点でOpenのままなんですね。
多くのuvユーザが待っていると思うんですが、半年間でも設計には足りなかったのでしょうか?(優秀なエンジニア集まってると思うんで着手したら速いはず。ということは着手していない??)
コミュニティの議論が盛り上がっているのが救いかなと思います。

(※uvのリリースを細かく追ってないので、タスクランナー的な機能がリリース済みでしたら教えてください)

もう1つ、Ruffにもあります。

Ruffが置き換えようとしているFlake8にはプラグインがあります。
コミュニティがFlake8プラグインを自由に開発できるため、Flake8は支持されてきた面があると言えるでしょう。

さてRuffですが、Flake8より高速という一点張りなんですよ。
Ruffがプラグインをサポートしたら私にはもうFlake8にこだわる理由はほぼないように思われる4のですが、Issueは2年間進展がありません。

uvのタスクランナーも、Ruffのプラグイン機構も、コミュニティが望んでいるのにAstral社はなぜ何もしないんでしょうね?(なにか都合でも悪いんでしょうか?)
python-build-standaloneが同じ状態にならないと私は言い切ることができません。

終わりに

python-build-standaloneのAstral社移管について思うことを書きました。

  • Gregoryさんありがとう。そしてAstral社も2024年の大部分をかけてpython-build-standaloneを引き継いでくれてありがとう
  • でもuvã‚„Ruffがコミュニティの要望が高い機能を放置しているように見えるんですが、python-build-standaloneでも繰り返さないと言えますか?(行動で示してほしいです)

この記事を書いた手前、私も緊張感を持って注視していきつつ、小さくともプルリクを送れるようになるべく、1日1エントリなど使っていきたいなあという気持ちです


  1. 念頭にあるのはこちら
  2. uv pythonコマンド https://docs.astral.sh/uv/reference/cli/#uv-python
  3. Gregory氏視点はこちら Gregory Szorc's Digital Home | Transferring Python Build Standalone Stewardship to Astral
  4. 似た意見 https://github.com/astral-sh/ruff/issues/283#issuecomment-2329103310

Python環境構築DASH村。python-build-standaloneのおかげで、curlかwgetだけあれば余裕です✌️

はじめに

「「「「けど」」」」 nikkieです。

知的好奇心を満たすための手作り、再び開催です!1

※これは学習用途なので、実際にはHatchやuvなどpython-build-standaloneを中で使っているツールを使ってください!

目次

python-build-standalone

創始者はGregory Szorc氏。
この12月にAstral社へ移管されました。

移管を発表したAstral社のブログにpython-build-standaloneがどんなものか書いてあります(「What's a "standalone" Python distribution?」)。

CPythonはstandaloneではない。
すなわち、ビルドしたシステムと強く結びつく

  • Linuxã‚„macOSでは、いくつかのシステムパスがバイナリにハードコードされる
  • いくつかのシステムライブラリに動的リンクする

なのでソースをダウンロードしてマシンでビルドするというアプローチが採られてきた(例:pyenv)わけです。
しかし、このアプローチには欠点もあります

  1. 事前にビルドされたバイナリのダウンロードと比べて、遅い
    • (IMO:ソースダウンロード + バイナリビルドの工程を考えたら時間はかかりますよね)
  2. ビルドツールチェーンの依存(例:gcc)を導入することになるが、失敗しうる
    • (IMO:経験がめっちゃあります...)

python-build-standaloneはこれらの問題に対処した、事前ビルド済みのバイナリです。
依存の静的リンクと、相対パスで動くようCPythonのビルドシステムへのパッチによって対処したそうです

Specifically, python-build-standalone solves these problems by (1) statically linking Python against its dependencies; and (2) patching the CPython build system to operate on relative, rather than absolute paths.

uv, Hatch, Ryeなど昨今はPython処理系も含めて管理するパッケージマネージャが出現していますが、これらの裏にはpython-build-standaloneがあるんです!
ということで、python-build-standaloneでPython環境を作ってみます。

python-build-standaloneでPythonをインストール!

参考にした記事はこちら

合わせてドキュメントも確認しました。

python-build-standaloneはGitHubのReleasesにバイナリが置かれます。
https://github.com/astral-sh/python-build-standalone/releases
ぶっちゃけここからファイルを指定してダウンロードしてくるだけです。

URLの命名規則ですが、「asset_url_prefix + ファイル名」です。

asset_url_prefix

% curl -L -s https://raw.githubusercontent.com/astral-sh/python-build-standalone/latest-release/latest-release.json | jq -r '.asset_url_prefix'
https://github.com/indygreg/python-build-standalone/releases/download/20241219

ファイル名は(今回はM1 Macなので)「cpython-3.12.8+20241219-aarch64-apple-darwin-install_only.tar.gz」

  • aarch64-apple-darwin
  • install-only2
    • Casual users will likely want to use the install_only archive, as most users do not need the build artifacts present in the full archive.

% wget 'https://github.com/indygreg/python-build-standalone/releases/download/20241219/cpython-3.12.8+20241219-aarch64-apple-darwin-install_only.tar.gz' -O distribution.tar.gz
% # または
% curl -L 'https://github.com/indygreg/python-build-standalone/releases/download/20241219/cpython-3.12.8+20241219-aarch64-apple-darwin-install_only.tar.gz' -o distribution.tar.gz

チェックサムを確認しておきましょう

% diff -s <(curl -s -L 'https://github.com/indygreg/python-build-standalone/releases/download/20241219/cpython-3.12.8+20241219-aarch64-apple-darwin-install_only.tar.gz.sha256') <(shasum -a 256 distribution.tar.gz | cut -d' ' -f1)
Files /dev/fd/11 and /dev/fd/12 are identical
% echo $?
0

解凍!

% tar xf distribution.tar.gz

pythonディレクトリができており

.
├── distribution.tar.gz
└── python/
    ├── bin/
    ├── include/
    ├── lib/
    └── share/
% python/bin/python3.12 -V
Python 3.12.8

処理系、入手!🙌 🙌 🙌

なにこれ! めっちゃ簡単になってますね。
このPython処理系をPATHが通ったところに配置すればよさそうに思われます(Hatchやuvをパクりたい)

python-build-standaloneで入手したPythonのpipでパッケージインストール

pipが同梱されている3のでパッケージのインストールもできます!

まずは仮想環境を作ります
(今回は後回しにしましたが、python/bin/python3.12がPATHの通った場所に置かれた後を想定しています)

% python/bin/python3.12 -m venv .venv --upgrade-deps
% .venv/bin/python -V
Python 3.12.8

(仮想環境をactivateしたらpython -Vだけで済みますが、ちょっとしたこだわりから今回はこちらでいきます)

% .venv/bin/python -m pip install kojo-fan-art
% .venv/bin/kojo-day kokoro
{"kokoro": "Tuesday"}

すんなりいきましたが、Zennの記事からつまづきポイントだと思っていました。
.venv/bin/python -m siteを見ます。

この状態なのですが、pip installしたライブラリは(USER_SITEではなく)仮想環境に置かれました。
仮想環境はsys.pathに含まれるので、kojo-dayコマンドが使えます。

この挙動を説明するドキュメントはまだ見つけられていないのですが、当初はZenn記事にならってPYTHONUSERBASEを指定していました。
https://docs.python.org/ja/3/using/cmdline.html#envvar-PYTHONUSERBASE
PYTHONUSERBASE=$PWD/.venv .venv/bin/python -m siteでUSER_SITEがsys.pathに含まれることを確認できます

% PYTHONUSERBASE=$PWD/.venv .venv/bin/python -m pip install kojo-fan-art  # 結果的に、PYTHONUSERBASEの指定は不要

(クリアにできていないところもありますが)python-build-standaloneでPython処理系を取得し、仮想環境を作ってそこにインストールできました!

終わりに

python-build-standaloneでPython環境を作る素振りでした

  • 命名規則に従ってGitHubのReleasesからダウンロードして解凍するだけ!
    • 宿題:今回のやり方だと毎回ディレクトリにPython処理系が置かれるので、マシンに1個にしたい(Hatchã‚„uvを見てみよう)
  • pipが同梱されており、仮想環境を作ってパッケージのインストールまでできた!
    • 宿題:USER_SITEが仮想環境ではないのに、pip installで仮想環境に入るのなんでだろう?

今後はcurlかwgetさえあれば、私はPython環境が作れると思います💪
2020年代のPython環境構築はこんなに便利になっているんですな〜(先人、特にGregoryさんに大感謝🫶🫶🫶)


  1. 前回のDASH村
  2. full archiveならpgo+ltoかなと思っています。「These should be the fastest distributions since they have the most build-time optimizations.」
  3. 意図はこちら「The intent of the pre-installed software is to facilitate end-user package installation without having to first bootstrap a packaging tool via an insecure installation technique (such as curl | sh patterns).」

pydantic-settingsで環境変数からもオプション引数を指定できるCLIを作る 〜サブコマンド篇〜

はじめに

幕が上がる 瞬間が好き♪ nikkieです。

pydantic-settingsでCLIの素振りの続き、今回はサブコマンドまわりを触ります。

目次

pydantic-settingsでCLIが作れる!

パースライブラリPydanticを設定にも適用するpydantic-settings。
なんと設定だけでなく、CLIアプリケーションも作れちゃいます!

pydantic-settingsで作るCLIのよさは、オプション引数を環境変数からも指定できること。
標準ライブラリのargparseでこれを実現するのはいささか骨が折れそうなのですが、pydantic-settingsではデフォルトでサポートされているんです!
argparseすきすきの民の私ですが、これは乗り換えを決意するのに十分すぎる理由でした(なので素振りをしています)

前回の素振りより、オプション引数・位置引数の扱いが分かっています。
pydantic_settings.BaseSettingsを継承したクラスにて

  • オプション引数:string: str = Field(validation_alias=AliasChoices("s", "string"))
    • -sã‚„--stringで指定できる
    • 環境変数Sã‚„STRINGでも指定できる(素晴らしすぎる!!👏)
    • デフォルト値を持たせることも可能(Field("デフォルト値", validation_alias=...))
  • 位置引数:string: CliPositionalArg[str]
    • 位置引数は環境変数からは指定できません(位置で指定しましょう)
    • 位置引数にデフォルト値は持たせられない模様1

サブコマンド

Rustのclapで素振りしたのと同様なCLIを今回作りました2。

% uv run --quiet cli.py position エミリー 5
エミリーエミリーエミリーエミリーエミリー
% OPTION_S=エミリー uv run --quiet cli.py option --num 5
エミリーエミリーエミリーエミリーエミリー

動作環境

  • uv 0.4.27 (Homebrew 2024-10-25)
  • Python 3.13.0
  • pydantic-settings 2.7.0
    • Pydantic 2.10.4

ドキュメント「Subcommands and Positional Arguments」より
https://docs.pydantic.dev/latest/concepts/pydantic_settings/#subcommands-and-positional-arguments

class Settings(BaseSettings):
    position: CliSubCommand[Position]
    option: CliSubCommand[Option]
  • サブコマンドの実体は
    • PydanticのBaseModelを継承したクラス
    • または、pydantic.dataclasses.dataclassでデコレートしたクラス
    • (IMO:サブコマンドにはBaseSettingsは継承しないんですね!)
  • BaseSettingsを継承したクラス(コマンドの大元)では、CliApp.get_subcommand()を使う
  • 設定のとき3と同様に、環境変数は「BaseSettings継承クラスの属性名」+「区切り文字」+「サブコマンドのエイリアス」でした
    • 区切り文字はenv_nested_delimiterでアンダースコアを指定
    • 列挙すると、OPTION_S・OPTION_STRING・OPTION_N・OPTION_NUM
  • (前回記事より)CliApp.run(Settings)でSettingsインスタンスのcli_cmd()メソッドを呼び出してます

サブコマンドもできた〜!🙌

なお、Noteに制約の記述が

it does not allow for multiple subparsers with each subparser having its own set of subcommands.

直面したわけではないのでピンときてはいないのですが、argparseみたいにサブコマンド・サブサブコマンドとネストさせられないってことですかね?
BaseSettingsを継承したクラスをCliSubCommandアノテーションできないようですし。
argparseでは使うことがあったのでちょっと痛いかもしれないですが、サブサブコマンドが必要になったときに考えようかと思います(サブコマンドだけに合わせて、コマンドの設計を見直すことになりそう)

終わりに

pydantic-settingsで作るCLIで、サブコマンドの作り方を完全に理解しました!
clapの素振りでも書きましたが、位置引数・オプション引数・サブコマンドを押さえたので、argparseで書いているCLIの8割くらいは、pydantic-settingsでも書けそうです(※馬鹿の山感)。

Pydanticですから書いてて型がバチバチ当たりますし、環境変数からオプションを指定できるのは広く使われるCLIは備えている印象のある便利機能で、pydantic-settingsは推したいライブラリになりました。
君が天才!


  1. argparse(やRustのclap)ではできていました。位置引数のデフォルト値をそれほど使っていない私としては、あまり痛くはないです
  2. uvがinline script metadataを読んでいるという出力(「Reading inline script metadata」)をしなくする--quietを見つけました。https://docs.astral.sh/uv/reference/cli/#uv-run
  3. 設定の素振りの様子:pydantic-settings素振りの記:ネストした設定 〜BaseModel継承クラスが全ての属性でデフォルト値を持つならば、インスタンス化してBaseSettings側のデフォルト値とする〜 - nikkie-ftnextの日記