requirements.txtを作るだけのアプリケーションを作った
実装機能
- フォルダ検索
- フォルダの詳細表示
- 複数のフォルダ選択
- 言語選択
- バージョンの書き込み
- パッケージの選択
requirements.txt とは
requirements.txt
は、プロジェクトを他のユーザーと共有したり、環境を復元したりするために、Python で使用されるテキストファイルです。このファイルでは、プロジェクトに必要な外部パッケージを指定します。
matplotlib==3.5.1
numpy==1.22.0
pandas==1.3.5
パッケージリストをrequirements.txt
に記録し、次のいずれかのコマンドを実行してパッケージをインストールします。オプションは-r:-requirement <file>です。
$ pip3 install -r requirements.txt
$ python3 -m pip install -r requirements.txt
パッケージをrequirements.txt
に記録するときにバージョンを指定することもできます。
メソッド | 例 |
---|---|
等しい | Django==3.0.3 |
以上 | Django>=3.0.3 |
互換性あり | Django~=3.0.3 |
現在の環境で外部パッケージのバージョンを表示するには、pipfreeze
コマンドを実行します。また、requirements.txt
に書き込むには、以下のコマンドを実行します。
$ pip freeze > requirements.txt
requirements.txt
は Julia にも同じように適応できます。Julia では、REPL で]
→ status
と入力して実行することにより、現在インストールされている外部パッケージとそのバージョンを確認できます。
$julia
julia > # press ]
(@v1.x) pkg> status
または、プログラムで Julia ファイルを実行します。
using Pkg; Pkg.status()
Julia は、バージョンの表記が Python と異なります。バージョンの表記は、==
ではなく@
で表されます。
CSV@v0.8.5
DataFrames@v0.22.7
Distributions@v0.24.18
Flux@v0.12.8
Genie@v3.0.0
IJulia@v1.23.2
Lathe@v0.1.6
Plots@v1.23.6
Julia でrequirements.txt
を使用してパッケージをインストールするには、以下のファイルを実行します。
using Pkg; Pkg.add(open(f->f.readlines(f), "requirements.txt"))
モジュールを抽出するクラスの作成
Python, Julia で共通のモジュール抽出を行うメソッドの作成
ソースコードの形式は Python、Julia とそれぞれの Jupyter Notebook の 4 つがあるので 4 つのメソッドを実装します。Python と Julia はモジュールを読み込む部分がかなり似ているので、同じ関数で処理をすることにします。
class ModuleExtractor:
def common(self, source: str, ipynb: bool = False) -> Tuple[Set[str], List[str]]:
called_function_name = str(inspect.stack()[1].function)
if ipynb:
source_list = []
ipynb_data: object = json.loads(source)
for cell in ipynb_data["cells"]:
source_list += cell["source"]
source = "".join(source_list)
language = "python" if "python" in called_function_name else "julia"
prefixes: list = settings.CONFIG["languages"][language][1]
embedded: list = settings.CONFIG["languages"][language][2]
process = [f"x.startswith('{prefix}')" for prefix in prefixes]
process_word = " or ".join(process)
splited_source = source.split("\n")
line_with_module = [x for x in splited_source if eval(process_word)]
modules = list(map(lambda m: m.split()[1], line_with_module))
modules = list(map(lambda m: m.replace(":", "").replace(";", ""), modules))
result = set(map(lambda m: m.split(".")[0] if not m.startswith(".") else "", modules))
return (result, embedded)
1 行目では、common
関数がどの関数から呼び出されたのか、その関数名を取得しています。これには標準ライブラリのinspect
を使います。
called_function_name = str(inspect.stack()[1].function)
以下の部分は、ファイルが Jupyter Notebook だった場合に行う処理です。
if ipynb:
source_list = []
ipynb_data: object = json.loads(source)
for cell in ipynb_data["cells"]:
source_list += cell["source"]
source = "".join(source_list)
.ipynb
ファイルは読み込むと json 形式になっていて、print('Hello, Wrold!')
を実行するだけのファイルの場合は以下のようになっています。
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": ["print('Hello, Wrold!')"]
}
],
"metadata": {
"language_info": {
"name": "python"
},
"orig_nbformat": 4
},
"nbformat": 4,
"nbformat_minor": 2
}
cells
→ source
でソースコードを取得できます。これをsource_list
という変数に格納していき、最終的に文字列に変換しています。これで Jupyter Notebook の場合とそうでない場合の違いがなくなりました。
次のコードはモジュールを読み込むための接頭辞と標準ライブラリを取得する部分になります。
language = "python" if "python" in called_function_name else "julia"
prefixes: list = settings.CONFIG["languages"][language][1]
embedded: list = settings.CONFIG["languages"][language][2]
まずは三項演算子を使って、言語を Python か Julia に分けます。
そして、設定が書かれたconfig.json
ファイルを読み込んだsettings.py
のCONFIG
から、接頭辞と標準ライブラリを取得します。
import os
TOP_DIR = os.path.dirname(__file__)
STATIC_DIR = os.path.join(TOP_DIR, "static")
with open(os.path.join(STATIC_DIR, "config.json"), "r") as config_file:
CONFIG = json.load(config_file)
CONFIG
は、以下のようになっています。
{
"languages": {
"python": [
"py",
[
"import",
"from"
],
[
"__future__",
"..."
]
],
"python-ipynb": [
"ipynb"
],
"julia": [
"jl",
[
"import",
"using"
],
[
"Base",
"..."
]
],
"julia-ipynb": [
"ipynb"
]
}
次にモジュールを取得する処理を書いていきます。
process = [f"x.startswith('{prefix}')" for prefix in prefixes]
process_word = " or ".join(process)
splited_source = source.split("\n")
line_with_module = [x for x in splited_source if eval(process_word)]
modules = list(map(lambda m: m.split()[1], line_with_module))
modules = list(map(lambda m: m.replace(":", "").replace(";", ""), modules))
result = set(map(lambda m: m.split(".")[0] if not m.startswith(".") else "", modules))
return (result, embedded)
まずは先ほどCONFIG
から読み込んだprefixes
を接頭辞をor
で繋げます。Python と Julia で接頭辞が異なるので、eval
で処理します。
ソースコードを改行で分割し、モジュールを読み込んでいる行を変数に格納します。さらに、モジュールを読み込んでいる行を空白で分割し、その 1 番目のインデックスを取得します。これはimport time
となっていた場合のtime
になります。ここで Julia のモジュール読み込み用に:
と;
を削除します。相対パスで読み込んでいる場合を考慮して、.
でさらに分割すると、その最初のインデックスがモジュールであると考えられるので、これらを集合にしてモジュールを一意に扱います。
最後に、抽出したモジュールの集合と標準ライブラリを、呼び出された関数に返します。
common
メソッドを呼び出し標準ライブラリを削除する各形式のメソッドの作成
ここからは、先ほどのcommon
メソッドを呼び出す、4 つのメソッドを実装します。
- Python
- Python(ipynb)
- Julia
- Julia(ipynb)
どのメソッドでも処理内容に変更はなく、モジュール抽出はcommon
で行ったので、ここではcommon
の返値のモジュールから標準ライブラリを削除する処理を行います。filter
関数を使って標準ライブラリではないものを、新しい集合にして返しています。
def python(self, source: str) -> Set[str]:
result, embedded_modules = self.common(source)
filtered_result = set(filter(lambda m: False if m in embedded_modules else m, result))
return filtered_result
def pythonipynb(self, ipynb_data: str) -> Set[str]:
result, embedded_modules = self.common(ipynb_data, ipynb=True)
filtered_result = set(filter(lambda m: False if m in embedded_modules else m, result))
return filtered_result
def julia(self, source: str) -> Set[str]:
result, embedded_modules = self.common(source)
filtered_result = set(filter(lambda m: False if m in embedded_modules else m, replaced_result))
return filtered_result
def juliaipynb(self, ipynb_data: str) -> Set[str]:
result, embedded_modules = self.common(ipynb_data, ipynb=True)
filtered_result = set(filter(lambda m: False if m in embedded_modules else m, replaced_result))
return filtered_result
これでモジュール抽出を行うクラスを作成することができました。
requirements.txt を作成するクラスの作成
ここからは requirements.txt を作成するための処理に加えて、アプリケーションの機能に必要な「詳細表示」と「パッケージ選択」を行うためのメソッドも作成します。具体的に以下を作成します。
- 選択されたフォルダから、その下にあるすべてのファイルとフォルダを取得するクラス
- バージョンを取得するメソッド
- バージョンが書かれたモジュールを返すメソッド
- requirements.txt を作成するメソッド
- 詳細情報を取得するメソッド
選択されたフォルダから、その下にあるすべてのファイルとフォルダを取得するクラスの作成
まず初めに、後ほど作成するRequirementsGenerator
クラスが継承するOperate
クラスの作成です。このOperate
は、選択されたフォルダから、その下にあるすべてのファイルとフォルダを取得するためのメソッドを有しています。
class Operate:
def get_directories(self, path: str) -> None:
parent: list = os.listdir(path)
directories = [f for f in parent if os.path.isdir(os.path.join(path, f))]
for dir in directories:
dir_full_path = os.path.join(path, dir)
self.all_directory.append(dir_full_path)
self.get_directories(dir_full_path)
def get_files(self, selected_lang: str) -> None:
if "ipynb" in selected_lang:
ipynb_index = selected_lang.find("ipynb")
selected_lang = f"{selected_lang[:ipynb_index]}-{selected_lang[ipynb_index:]}"
for dir in self.all_directory:
parent: list = os.listdir(dir)
files = [f for f in parent if os.path.isfile(os.path.join(dir, f))]
filtered_files_path = list(filter(lambda path: path.endswith(settings.CONFIG["languages"][selected_lang][0]), files))
file_full_path = list(map(lambda path: os.path.join(dir, path), filtered_files_path))
self.all_file += file_full_path
get_directories
メソッドは、引数に選択されたフォルダの絶対パスを取得します。これをos.listdir()
に渡すことで、選択されたフォルダパスの直下にあるフォルダを、リストで取得することができます。取得したリストには隠しファイルが含まれることがあるのでos.path.isdir
を使ってフォルダであるもののみを取得します。ここまでで取得できたリストに含まれる文字列は、選択されたフォルダパスからの相対パスなのでos.path.join()
を使って、絶対パスにします。そしてこのget_directories
を再帰的に呼び出すことで、フォルダの下にあるフォルダの情報も、全て取得することができます。
def get_directories(self, path: str) -> None:
parent: list = os.listdir(path)
directories = [f for f in parent if os.path.isdir(os.path.join(path, f))]
for dir in directories:
dir_full_path = os.path.join(path, dir)
self.all_directory.append(dir_full_path)
self.get_directories(dir_full_path)
次にget_files
メソッドを作成します。ここでは先ほどのget_directories
メソッドを実行した上で得られる、全てのフォルダの中に含まれる、全てのファイルを取得します。しかし、選択された言語の拡張子と一致するファイルのみの取得になります。
def get_files(self, selected_lang: str) -> None:
if "ipynb" in selected_lang:
ipynb_index = selected_lang.find("ipynb")
selected_lang = f"{selected_lang[:ipynb_index]}-{selected_lang[ipynb_index:]}"
for dir in self.all_directory:
parent: list = os.listdir(dir)
files = [f for f in parent if os.path.isfile(os.path.join(dir, f))]
filtered_files_path = list(filter(lambda path: path.endswith(settings.CONFIG["languages"][selected_lang][0]), files))
file_full_path = list(map(lambda path: os.path.join(dir, path), filtered_files_path))
self.all_file += file_full_path
初めの 3 行は、アプリケーションに表示させる言語選択の文字が、Jupyter Notebook の場合「Python(ipynb)」としていて、そのvalue
が「python-ipynb」となっているので、config.json
ファイルと合わせるためにフォーマットを揃えています。次のfor
の中ではget_directories
と同じように、全てのフォルダ内に含まれるファイルを取得しています。
RequirementsGenerator クラスの作成
class RequirementsGenerator(Operate):
def __init__(self, path: str = None, lang: str = None, version: bool = False) -> None:
self.path = "" if path is None else path
self.lang = "" if lang is None else lang
self.all_file = []
self.all_directory = [path]
if version:
if "python" in self.lang:
stdout_result_splited = self.command_runner(["pip3", "freeze"])
self.installed_modules = [x for x in stdout_result_splited if "==" in x]
self.version_split_word = "=="
self.module_match_process_word = "module.replace('_', '-') == module_name.lower()"
elif "julia" in self.lang:
stdout_result_splited = self.command_runner(["julia", "-e", "using Pkg; Pkg.status()"])
installed_packages = list(map(lambda x: x.lstrip(" "), stdout_result_splited))
installed_packages.remove("")
self.installed_modules = ["@".join(package_info.split(" ")[1:]) for package_info in installed_packages[1:]]
self.version_split_word = "@"
self.module_match_process_word = "module == module_name"
def command_runner(self, command: List[str]) -> List[str]:
stdout_result = subprocess.run(command, capture_output=True)
stdout_result_splited = stdout_result.stdout.decode("utf-8").split("\n")
return stdout_result_splited
def confirm(self) -> List[str]:
self.get_directories(self.path)
self.get_files(self.lang)
module_extractor = ModuleExtractor()
modules_for_return = set()
for file_path in self.all_file:
with open(file_path, "r", encoding="utf-8") as file:
file_contents = file.read()
modules_for_return = modules_for_return.union(getattr(module_extractor, self.lang)(file_contents))
if hasattr(self, "installed_modules"):
tmp_modules = set()
matched_modules = set()
for module in modules_for_return:
for installed_module in self.installed_modules:
module_name = installed_module.split(self.version_split_word)[0] # Note: Used in eval
if eval(self.module_match_process_word):
matched_modules.add(module)
tmp_modules.add(installed_module)
else:
tmp_modules.add(module)
module_list = list(tmp_modules)
for matched_module in matched_modules:
try:
module_list.remove(matched_module)
except ValueError as ex:
print(f"Error: {ex}")
else:
module_list = list(modules_for_return)
module_list.sort()
return module_list
def generate(self, module_list: List[str]) -> None:
module_list = list(map(lambda x: x.replace("\n", ""), module_list))
with open(os.path.join(self.path, "requirements.txt"), "w", encoding="utf-8") as f:
modules = "\n".join(module_list)
f.write(modules)
def detail(self, directories: List[str]) -> Dict[str, Dict[str, int]]:
result = {}
for dir in directories:
supported_extension = {"py": 0, "jl": 0, "go": 0, "ipynb": 0, "other": 0}
if self.all_directory.count(""):
self.all_directory.remove("")
self.all_directory.append(dir)
self.get_directories(dir)
for middle_dir in self.all_directory:
parent: list = os.listdir(middle_dir)
try:
files = [f for f in parent if os.path.isfile(os.path.join(middle_dir, f))]
for extension in supported_extension:
supported_extension[extension] += len(list(filter(lambda f: f.endswith(extension), files)))
except TypeError as ex:
print(f"Error: {ex}")
extension_counted = [v for v in supported_extension.values()]
sum_extension_counted = sum(extension_counted)
if 0 < sum_extension_counted:
supported_extension = {k: round((v / sum_extension_counted) * 100, 2) for k, v in zip(supported_extension, extension_counted)}
else:
supported_extension["other"] = 100
display_dir_name = dir.split("/")[-1]
result[display_dir_name] = supported_extension
self.all_directory.clear()
return result
外部パッケージのバージョンを取得するメソッドの作成
__init__
では、変数の初期化と、アプリケーションでバージョンを表示させるかどうかのチェックボックスで、true
だった場合に、このアプリケーションが実行されている環境にインストールされた外部パッケージを取得する処理を実装しています。Python であればpip3 freeze
でバージョンを標準出力させ、その結果を文字列として取得します。Julia も同様に、-e
オプションを使用して、標準出力からバージョンを取得しています。
def __init__(self, path: str = None, lang: str = None, version: bool = False) -> None:
self.path = "" if path is None else path
self.lang = "" if lang is None else lang
self.all_file = []
self.all_directory = [path]
if version:
if "python" in self.lang:
stdout_result_splited = self.command_runner(["pip3", "freeze"])
self.installed_modules = [x for x in stdout_result_splited if "==" in x]
self.version_split_word = "=="
self.module_match_process_word = "module.replace('_', '-') == module_name.lower()"
elif "julia" in self.lang:
stdout_result_splited = self.command_runner(["julia", "-e", "using Pkg; Pkg.status()"])
installed_packages = list(map(lambda x: x.lstrip(" "), stdout_result_splited))
installed_packages.remove("")
self.installed_modules = ["@".join(package_info.split(" ")[1:]) for package_info in installed_packages[1:]]
self.version_split_word = "@"
self.module_match_process_word = "module == module_name"
コマンドを実行するメソッド ↓
def command_runner(self, command: List[str]) -> List[str]:
stdout_result = subprocess.run(command, capture_output=True)
stdout_result_splited = stdout_result.stdout.decode("utf-8").split("\n")
return stdout_result_splited
バージョンが書かれたモジュールを返すメソッドの作成
def confirm(self) -> List[str]:
self.get_directories(self.path)
self.get_files(self.lang)
module_extractor = ModuleExtractor()
modules_for_return = set()
for file_path in self.all_file:
with open(file_path, "r", encoding="utf-8") as file:
file_contents = file.read()
modules_for_return = modules_for_return.union(getattr(module_extractor, self.lang)(file_contents))
if hasattr(self, "installed_modules"):
tmp_modules = set()
matched_modules = set()
for module in modules_for_return:
for installed_module in self.installed_modules:
module_name = installed_module.split(self.version_split_word)[0] # Note: Used in eval
if eval(self.module_match_process_word):
matched_modules.add(module)
tmp_modules.add(installed_module)
else:
tmp_modules.add(module)
module_list = list(tmp_modules)
for matched_module in matched_modules:
try:
module_list.remove(matched_module)
except ValueError as ex:
print(f"Error: {ex}")
else:
module_list = list(modules_for_return)
module_list.sort()
return module_list
まず初めに選択されたフォルダ内のフォルダ・ファイルの情報をすべて取得します。
self.get_directories(self.path)
self.get_files(self.lang)
次にModuleExtractor
クラスのインスタンスを作成して、getattr()
で選択された言語と対応したメソッドを呼び出します。
module_extractor = ModuleExtractor()
modules_for_return = set()
for file_path in self.all_file:
with open(file_path, "r", encoding="utf-8") as file:
file_contents = file.read()
modules_for_return = modules_for_return.union(getattr(module_extractor, self.lang)(file_contents))
クラス内にinstalled_modules
という変数が存在するかどうかをhasattr
で調べます。変数が存在しなかった場合は集合をリストにして、ソートしてアプリケーションに渡します。変数が存在する場合は、選択されたフォルダから取得したモジュールと、環境にインストールされたモジュールが一致しているものを集合に格納していきます。
if hasattr(self, "installed_modules"):
tmp_modules = set()
matched_modules = set()
for module in modules_for_return:
for installed_module in self.installed_modules:
module_name = installed_module.split(self.version_split_word)[0] # Note: Used in eval
if eval(self.module_match_process_word):
matched_modules.add(module)
tmp_modules.add(installed_module)
else:
tmp_modules.add(module)
module_list = list(tmp_modules)
for matched_module in matched_modules:
try:
module_list.remove(matched_module)
except ValueError as ex:
print(f"Error: {ex}")
else:
module_list = list(modules_for_return)
module_list.sort()
return module_list
requirements.txt を作成するメソッドの作成
アプリケーションでパッケージが選択され、それをリストとして引数に受け取るgenerate
メソッドは、選択されたパスにrequirements.txt
を生成します。
def generate(self, module_list: List[str]) -> None:
module_list = list(map(lambda x: x.replace("\n", ""), module_list))
with open(os.path.join(self.path, "requirements.txt"), "w", encoding="utf-8") as f:
modules = "\n".join(module_list)
f.write(modules)
詳細情報を取得するメソッドの作成
detail
は、アプリケーションで詳細表示ボタンが押された時に実行されるメソッドです。これはkey
に選択された絶対パスと、value
にそのフォルダ直下にあるファイルの数を返します。ファイルは対応言語とそれ以外という区分になっています。処理自体は単純で、パス直下の全てのファイルを取得し、拡張子の数をカウントするだけです。
def detail(self, directories: List[str]) -> Dict[str, Dict[str, int]]:
result = {}
for dir in directories:
supported_extension = {"py": 0, "jl": 0, "go": 0, "ipynb": 0, "other": 0}
if self.all_directory.count(""):
self.all_directory.remove("")
self.all_directory.append(dir)
self.get_directories(dir)
for middle_dir in self.all_directory:
parent: list = os.listdir(middle_dir)
try:
files = [f for f in parent if os.path.isfile(os.path.join(middle_dir, f))]
for extension in supported_extension:
supported_extension[extension] += len(list(filter(lambda f: f.endswith(extension), files)))
except TypeError as ex:
print(f"Error: {ex}")
extension_counted = [v for v in supported_extension.values()]
sum_extension_counted = sum(extension_counted)
if 0 < sum_extension_counted:
supported_extension = {k: round((v / sum_extension_counted) * 100, 2) for k, v in zip(supported_extension, extension_counted)}
else:
supported_extension["other"] = 100
display_dir_name = dir.split("/")[-1]
result[display_dir_name] = supported_extension
self.all_directory.clear()
return result
デスクトップ直下のフォルダツリーを作成する関数
Desktop の絶対パスをos.walk()
に渡すと、全てのフォルダの全てのパスを取得することができます。実はOperate
クラスのget_directories
とget_files
の処理はこれで置き換えることができます。このos.walk()
で取得した最初のインデックスには、フォルダの絶対パスが格納されています。もしこのパスが.git
や__pycache__
などの場合は、requirements.txt
に必要のないフォルダとなるので、それ以外のパスでフォルダツリーの情報を取得していきます。
def generate_tree() -> None:
tree_data = {"data": []}
for directory_stracture in os.walk(settings.DESKTOP_PATH):
tree_information = {}
dir_path = directory_stracture[0]
if not list(filter(lambda x: True if x in dir_path else False, settings.IGNORE_DIRECTORIES)):
dir_list = dir_path.split(settings.PATH_SPLIT_WORD)
tree_information["id"] = dir_path
tree_information["text"] = dir_list[-1]
tree_information["parent"] = settings.PATH_SPLIT_WORD.join(dir_list[:-1])
if settings.DESKTOP_PATH == dir_path:
tree_information["parent"] = "#"
tree_data["data"].append(tree_information)
with open(settings.TREE_PATH, "w", encoding="utf-8") as f:
json.dump(tree_data, f, ensure_ascii=False, indent=2)
今回アプリケーションのツリー構造を表示させるのに使用しているのはtree.js
で、これがツリーを構成するのに必要な情報は、親のフォルダと、親フォルダからの相対パス、作成するフォルダツリーで一意である必要がある ID です。ID には絶対パスを割り当てています。しかし、親のないフォルダは、親に#
を指定する必要があり、今回はDesktop
直下のフォルダツリーを構成するため、絶対パスがDesktop
であった場合の親を#
にしています。これを辞書に格納していき、json.dump()
でjson
ファイルに変換しています。
Discussion