From 5ae3158a255f969bb2d610f6a1bcc814e9b70522 Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Mon, 9 Jun 2025 20:19:59 +0100 Subject: [PATCH 1/2] Add pre-commit --- .github/workflows/pre-commit.yml | 18 +++++ .github/workflows/update-lint-and-build.yml | 2 +- .pre-commit-config.yaml | 35 +++++++++ license.md | 2 +- manage_translation.py | 80 ++++++++++++--------- update_switcher_chart.py | 12 ++-- 6 files changed, 106 insertions(+), 43 deletions(-) create mode 100644 .github/workflows/pre-commit.yml create mode 100644 .pre-commit-config.yaml diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 000000000..eccafe60e --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,18 @@ +name: pre-commit +on: + schedule: + - cron: '0 0 * * 1,5' +env: + FORCE_COLOR: 1 + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: actions/setup-python@v5 + with: + python-version: "3.x" + - uses: tox-dev/action-pre-commit-uv@v1 diff --git a/.github/workflows/update-lint-and-build.yml b/.github/workflows/update-lint-and-build.yml index 91e12dfa8..47739abe0 100644 --- a/.github/workflows/update-lint-and-build.yml +++ b/.github/workflows/update-lint-and-build.yml @@ -54,7 +54,7 @@ jobs: - run: git config --local user.name "GitHub Action's update-translation job" - name: Check changes significance run: > - ! git diff -I'^"POT-Creation-Date: ' -I'^"Language-Team: ' -I'^# ' -I'^"Last-Translator: ' -I'^"Project-Id-Version: ' --exit-code && echo "SIGNIFICANT_CHANGES=1" >> $GITHUB_ENV || exit 0 + ! git diff -I'^"POT-Creation-Date: ' -I'^"Language-Team: ' -I'^# ' -I'^"Last-Translator: ' -I'^"Project-Id-Version: ' --exit-code && echo "SIGNIFICANT_CHANGES=1" >> "$GITHUB_ENV" || exit 0 - run: git add . - run: git commit -m 'Update translation from Transifex' if: env.SIGNIFICANT_CHANGES diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..28b721f45 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,35 @@ +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.11.4 + hooks: + - id: ruff + args: [--fix, --exit-non-zero-on-fix] + - id: ruff-format + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: check-added-large-files + - id: check-case-conflict + - id: check-merge-conflict + - id: check-yaml + - id: debug-statements + - id: end-of-file-fixer + exclude: ^\.tx/ + - id: forbid-submodules + - id: trailing-whitespace + + - repo: https://github.com/python-jsonschema/check-jsonschema + rev: 0.32.1 + hooks: + - id: check-github-workflows + + - repo: https://github.com/rhysd/actionlint + rev: v1.7.7 + hooks: + - id: actionlint + + - repo: meta + hooks: + - id: check-hooks-apply + - id: check-useless-excludes diff --git a/license.md b/license.md index b583b1a4c..a11a3d20b 100644 --- a/license.md +++ b/license.md @@ -1,4 +1,4 @@ **License** -By inviting you to work on a project on the Transifex platform, we offer a contract for donating your translations to the Python Software Foundation under the CC0 license. +By inviting you to work on a project on the Transifex platform, we offer a contract for donating your translations to the Python Software Foundation under the CC0 license. In return, it will be visible that you are the translator of the part you translated. You signify your acceptance of this agreement by submitting your work for inclusion in the documentation. diff --git a/manage_translation.py b/manage_translation.py index 86f5fcacd..8a5bf48b2 100755 --- a/manage_translation.py +++ b/manage_translation.py @@ -28,7 +28,7 @@ from polib import pofile from transifex.api import transifex_api -LANGUAGE = 'pl' +LANGUAGE = "pl" def fetch(): @@ -39,9 +39,9 @@ def fetch(): sys.stderr.write("The Transifex client app is required.\n") exit(code) lang = LANGUAGE - _call(f'tx pull -l {lang} --minimum-perc=1 --force --skip') - for file in Path().rglob('*.po'): - _call(f'msgcat --no-location -o {file} {file}') + _call(f"tx pull -l {lang} --minimum-perc=1 --force --skip") + for file in Path().rglob("*.po"): + _call(f"msgcat --no-location -o {file} {file}") def _call(command: str): @@ -49,8 +49,8 @@ def _call(command: str): exit(return_code) -PROJECT_SLUG = 'python-newest' -VERSION = '3.14' +PROJECT_SLUG = "python-newest" +VERSION = "3.14" def recreate_tx_config(): @@ -61,32 +61,36 @@ def recreate_tx_config(): with chdir(directory): _clone_cpython_repo(VERSION) _build_gettext() - with chdir(Path(directory) / 'cpython/Doc/build'): + with chdir(Path(directory) / "cpython/Doc/build"): _create_txconfig() _update_txconfig_resources() - with open('.tx/config', 'r') as file: + with open(".tx/config", "r") as file: contents = file.read() - contents = contents.replace('.//LC_MESSAGES/', '') - with open('.tx/config', 'w') as file: + contents = contents.replace(".//LC_MESSAGES/", "") + with open(".tx/config", "w") as file: file.write(contents) warn_about_files_to_delete() + def warn_about_files_to_delete(): files = list(_get_files_to_delete()) if not files: return - warn(f'Found {len(files)} file(s) to delete: {", ".join(files)}.') + warn(f"Found {len(files)} file(s) to delete: {', '.join(files)}.") + def _get_files_to_delete(): - with open('.tx/config') as config_file: + with open(".tx/config") as config_file: config = config_file.read() - for file in Path().rglob('*.po'): + for file in Path().rglob("*.po"): if os.fsdecode(file) not in config: yield os.fsdecode(file) def _clone_cpython_repo(version: str): - _call(f'git clone -b {version} --single-branch https://github.com/python/cpython.git --depth 1') + _call( + f"git clone -b {version} --single-branch https://github.com/python/cpython.git --depth 1" + ) def _build_gettext(): @@ -94,13 +98,13 @@ def _build_gettext(): def _create_txconfig(): - _call('sphinx-intl create-txconfig') + _call("sphinx-intl create-txconfig") def _update_txconfig_resources(): _call( - f'sphinx-intl update-txconfig-resources --transifex-organization-name python-doc ' - f'--transifex-project-name={PROJECT_SLUG} --locale-dir . --pot-dir gettext' + f"sphinx-intl update-txconfig-resources --transifex-organization-name python-doc " + f"--transifex-project-name={PROJECT_SLUG} --locale-dir . --pot-dir gettext" ) @@ -115,32 +119,34 @@ class ResourceLanguageStatistics: @classmethod def from_api_entry(cls, data: transifex_api.ResourceLanguageStats) -> Self: return cls( - name=data.id.removeprefix(f'o:python-doc:p:{PROJECT_SLUG}:r:').removesuffix(f':l:{LANGUAGE}'), - total_words=data.attributes['total_words'], - translated_words=data.attributes['translated_words'], - total_strings=data.attributes['total_strings'], - translated_strings=data.attributes['translated_strings'], + name=data.id.removeprefix(f"o:python-doc:p:{PROJECT_SLUG}:r:").removesuffix( + f":l:{LANGUAGE}" + ), + total_words=data.attributes["total_words"], + translated_words=data.attributes["translated_words"], + total_strings=data.attributes["total_strings"], + translated_strings=data.attributes["translated_strings"], ) def _get_tx_token() -> str: - if os.path.exists('.tx/api-key'): - with open('.tx/api-key') as f: + if os.path.exists(".tx/api-key"): + with open(".tx/api-key") as f: transifex_api_key = f.read() else: - transifex_api_key = os.getenv('TX_TOKEN', '') + transifex_api_key = os.getenv("TX_TOKEN", "") return transifex_api_key def _get_resources() -> list[transifex_api.Resource]: transifex_api.setup(auth=_get_tx_token()) - return transifex_api.Resource.filter(project=f'o:python-doc:p:{PROJECT_SLUG}').all() + return transifex_api.Resource.filter(project=f"o:python-doc:p:{PROJECT_SLUG}").all() def get_resource_language_stats() -> list[ResourceLanguageStatistics]: transifex_api.setup(auth=_get_tx_token()) resources = transifex_api.ResourceLanguageStats.filter( - project=f'o:python-doc:p:{PROJECT_SLUG}', language=f'l:{LANGUAGE}' + project=f"o:python-doc:p:{PROJECT_SLUG}", language=f"l:{LANGUAGE}" ).all() return [ResourceLanguageStatistics.from_api_entry(entry) for entry in resources] @@ -159,10 +165,10 @@ def get_number_of_translators(): def _fetch_translators() -> Generator[str, None, None]: - for file in Path().rglob('*.po'): + for file in Path().rglob("*.po"): header = pofile(file).header.splitlines() - for translator_record in header[header.index('Translators:') + 1:]: - translator, _year = translator_record.split(', ') + for translator_record in header[header.index("Translators:") + 1 :]: + translator, _year = translator_record.split(", ") yield translator @@ -174,7 +180,9 @@ def _eliminate_aliases(translators: set[str]) -> set[str]: unique = set() for name in translators: for match in unique: - if (ratio := SequenceMatcher(lambda x: x in '<>@', name, match).ratio()) > 0.64: + if ( + ratio := SequenceMatcher(lambda x: x in "<>@", name, match).ratio() + ) > 0.64: info(f"{name} and {match} are similar ({ratio:.3f}). Deduplicating.") break else: @@ -183,15 +191,17 @@ def _eliminate_aliases(translators: set[str]) -> set[str]: def language_switcher(entry: ResourceLanguageStatistics) -> bool: - language_switcher_resources_prefixes = ('bugs', 'tutorial', 'library--functions') - return any(entry.name.startswith(prefix) for prefix in language_switcher_resources_prefixes) + language_switcher_resources_prefixes = ("bugs", "tutorial", "library--functions") + return any( + entry.name.startswith(prefix) for prefix in language_switcher_resources_prefixes + ) if __name__ == "__main__": - RUNNABLE_SCRIPTS = ('fetch', 'recreate_tx_config', 'warn_about_files_to_delete') + RUNNABLE_SCRIPTS = ("fetch", "recreate_tx_config", "warn_about_files_to_delete") parser = ArgumentParser() - parser.add_argument('cmd', choices=RUNNABLE_SCRIPTS) + parser.add_argument("cmd", choices=RUNNABLE_SCRIPTS) options = parser.parse_args() eval(options.cmd)() diff --git a/update_switcher_chart.py b/update_switcher_chart.py index 7ddca16b3..4e0b79c85 100644 --- a/update_switcher_chart.py +++ b/update_switcher_chart.py @@ -7,18 +7,18 @@ from matplotlib import pyplot from matplotlib.ticker import PercentFormatter -repo = Repo('.') +repo = Repo(".") progress, dates = [], [] for commit in repo.iter_commits(): try: - readme_content = repo.git.show('{}:{}'.format(commit.hexsha, 'README.md')) + readme_content = repo.git.show("{}:{}".format(commit.hexsha, "README.md")) except GitCommandError: continue - found = search(r'!\[(\d\d.\d\d)% przełącznika języków]', readme_content) + found = search(r"!\[(\d\d.\d\d)% przełącznika języków]", readme_content) if not found: - found = search(r'!\[(\d+.\d\d)% language switchera]', readme_content) + found = search(r"!\[(\d+.\d\d)% language switchera]", readme_content) if not found: - found = search(r'!\[(\d+.\d\d)% do language switchera]', readme_content) + found = search(r"!\[(\d+.\d\d)% do language switchera]", readme_content) if not found: print(readme_content) continue @@ -26,7 +26,7 @@ progress.append(number) dates.append(datetime.fromtimestamp(commit.authored_date)) -pyplot.plot_date(dates, progress, linestyle='-', marker='') +pyplot.plot_date(dates, progress, linestyle="-", marker="") pyplot.ylim(ymin=0) pyplot.grid() pyplot.gcf().autofmt_xdate() From 0fee92ad20ef17fb0661936aeba6b45e02fb296b Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Tue, 10 Jun 2025 16:33:02 +0100 Subject: [PATCH 2/2] Update config --- manage_translation.py | 82 ++++++++++++++++++++-------------------- ruff.toml | 2 + update_switcher_chart.py | 35 ----------------- 3 files changed, 43 insertions(+), 76 deletions(-) create mode 100644 ruff.toml delete mode 100644 update_switcher_chart.py diff --git a/manage_translation.py b/manage_translation.py index 8a5bf48b2..7d53b2f23 100755 --- a/manage_translation.py +++ b/manage_translation.py @@ -28,20 +28,20 @@ from polib import pofile from transifex.api import transifex_api -LANGUAGE = "pl" +LANGUAGE = 'pl' def fetch(): """ Fetch translations from Transifex, remove source lines. """ - if (code := call("tx --version", shell=True)) != 0: - sys.stderr.write("The Transifex client app is required.\n") + if (code := call('tx --version', shell=True)) != 0: + sys.stderr.write('The Transifex client app is required.\n') exit(code) lang = LANGUAGE - _call(f"tx pull -l {lang} --minimum-perc=1 --force --skip") - for file in Path().rglob("*.po"): - _call(f"msgcat --no-location -o {file} {file}") + _call(f'tx pull -l {lang} --minimum-perc=1 --force --skip') + for file in Path().rglob('*.po'): + _call(f'msgcat --no-location -o {file} {file}') def _call(command: str): @@ -49,8 +49,8 @@ def _call(command: str): exit(return_code) -PROJECT_SLUG = "python-newest" -VERSION = "3.14" +PROJECT_SLUG = 'python-newest' +VERSION = '3.14' def recreate_tx_config(): @@ -61,13 +61,13 @@ def recreate_tx_config(): with chdir(directory): _clone_cpython_repo(VERSION) _build_gettext() - with chdir(Path(directory) / "cpython/Doc/build"): + with chdir(Path(directory) / 'cpython/Doc/build'): _create_txconfig() _update_txconfig_resources() - with open(".tx/config", "r") as file: + with open('.tx/config', 'r') as file: contents = file.read() - contents = contents.replace(".//LC_MESSAGES/", "") - with open(".tx/config", "w") as file: + contents = contents.replace('.//LC_MESSAGES/', '') + with open('.tx/config', 'w') as file: file.write(contents) warn_about_files_to_delete() @@ -76,35 +76,35 @@ def warn_about_files_to_delete(): files = list(_get_files_to_delete()) if not files: return - warn(f"Found {len(files)} file(s) to delete: {', '.join(files)}.") + warn(f'Found {len(files)} file(s) to delete: {", ".join(files)}.') def _get_files_to_delete(): - with open(".tx/config") as config_file: + with open('.tx/config') as config_file: config = config_file.read() - for file in Path().rglob("*.po"): + for file in Path().rglob('*.po'): if os.fsdecode(file) not in config: yield os.fsdecode(file) def _clone_cpython_repo(version: str): _call( - f"git clone -b {version} --single-branch https://github.com/python/cpython.git --depth 1" + f'git clone -b {version} --single-branch https://github.com/python/cpython.git --depth 1' ) def _build_gettext(): - _call("make -C cpython/Doc/ gettext") + _call('make -C cpython/Doc/ gettext') def _create_txconfig(): - _call("sphinx-intl create-txconfig") + _call('sphinx-intl create-txconfig') def _update_txconfig_resources(): _call( - f"sphinx-intl update-txconfig-resources --transifex-organization-name python-doc " - f"--transifex-project-name={PROJECT_SLUG} --locale-dir . --pot-dir gettext" + f'sphinx-intl update-txconfig-resources --transifex-organization-name python-doc ' + f'--transifex-project-name={PROJECT_SLUG} --locale-dir . --pot-dir gettext' ) @@ -119,34 +119,34 @@ class ResourceLanguageStatistics: @classmethod def from_api_entry(cls, data: transifex_api.ResourceLanguageStats) -> Self: return cls( - name=data.id.removeprefix(f"o:python-doc:p:{PROJECT_SLUG}:r:").removesuffix( - f":l:{LANGUAGE}" + name=data.id.removeprefix(f'o:python-doc:p:{PROJECT_SLUG}:r:').removesuffix( + f':l:{LANGUAGE}' ), - total_words=data.attributes["total_words"], - translated_words=data.attributes["translated_words"], - total_strings=data.attributes["total_strings"], - translated_strings=data.attributes["translated_strings"], + total_words=data.attributes['total_words'], + translated_words=data.attributes['translated_words'], + total_strings=data.attributes['total_strings'], + translated_strings=data.attributes['translated_strings'], ) def _get_tx_token() -> str: - if os.path.exists(".tx/api-key"): - with open(".tx/api-key") as f: + if os.path.exists('.tx/api-key'): + with open('.tx/api-key') as f: transifex_api_key = f.read() else: - transifex_api_key = os.getenv("TX_TOKEN", "") + transifex_api_key = os.getenv('TX_TOKEN', '') return transifex_api_key def _get_resources() -> list[transifex_api.Resource]: transifex_api.setup(auth=_get_tx_token()) - return transifex_api.Resource.filter(project=f"o:python-doc:p:{PROJECT_SLUG}").all() + return transifex_api.Resource.filter(project=f'o:python-doc:p:{PROJECT_SLUG}').all() def get_resource_language_stats() -> list[ResourceLanguageStatistics]: transifex_api.setup(auth=_get_tx_token()) resources = transifex_api.ResourceLanguageStats.filter( - project=f"o:python-doc:p:{PROJECT_SLUG}", language=f"l:{LANGUAGE}" + project=f'o:python-doc:p:{PROJECT_SLUG}', language=f'l:{LANGUAGE}' ).all() return [ResourceLanguageStatistics.from_api_entry(entry) for entry in resources] @@ -165,15 +165,15 @@ def get_number_of_translators(): def _fetch_translators() -> Generator[str, None, None]: - for file in Path().rglob("*.po"): + for file in Path().rglob('*.po'): header = pofile(file).header.splitlines() - for translator_record in header[header.index("Translators:") + 1 :]: - translator, _year = translator_record.split(", ") + for translator_record in header[header.index('Translators:') + 1 :]: + translator, _year = translator_record.split(', ') yield translator def _remove_bot(translators: set[str]) -> None: - translators.remove("Transifex Bot <>") + translators.remove('Transifex Bot <>') def _eliminate_aliases(translators: set[str]) -> set[str]: @@ -181,9 +181,9 @@ def _eliminate_aliases(translators: set[str]) -> set[str]: for name in translators: for match in unique: if ( - ratio := SequenceMatcher(lambda x: x in "<>@", name, match).ratio() + ratio := SequenceMatcher(lambda x: x in '<>@', name, match).ratio() ) > 0.64: - info(f"{name} and {match} are similar ({ratio:.3f}). Deduplicating.") + info(f'{name} and {match} are similar ({ratio:.3f}). Deduplicating.') break else: unique.add(name) @@ -191,17 +191,17 @@ def _eliminate_aliases(translators: set[str]) -> set[str]: def language_switcher(entry: ResourceLanguageStatistics) -> bool: - language_switcher_resources_prefixes = ("bugs", "tutorial", "library--functions") + language_switcher_resources_prefixes = ('bugs', 'tutorial', 'library--functions') return any( entry.name.startswith(prefix) for prefix in language_switcher_resources_prefixes ) -if __name__ == "__main__": - RUNNABLE_SCRIPTS = ("fetch", "recreate_tx_config", "warn_about_files_to_delete") +if __name__ == '__main__': + RUNNABLE_SCRIPTS = ('fetch', 'recreate_tx_config', 'warn_about_files_to_delete') parser = ArgumentParser() - parser.add_argument("cmd", choices=RUNNABLE_SCRIPTS) + parser.add_argument('cmd', choices=RUNNABLE_SCRIPTS) options = parser.parse_args() eval(options.cmd)() diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 000000000..749ef043d --- /dev/null +++ b/ruff.toml @@ -0,0 +1,2 @@ +[format] +quote-style = "single" diff --git a/update_switcher_chart.py b/update_switcher_chart.py deleted file mode 100644 index 4e0b79c85..000000000 --- a/update_switcher_chart.py +++ /dev/null @@ -1,35 +0,0 @@ -# EOL code, saving as this can be repurposed for a chart with the total translation progress - -from datetime import datetime -from re import search - -from git import Repo, GitCommandError -from matplotlib import pyplot -from matplotlib.ticker import PercentFormatter - -repo = Repo(".") -progress, dates = [], [] -for commit in repo.iter_commits(): - try: - readme_content = repo.git.show("{}:{}".format(commit.hexsha, "README.md")) - except GitCommandError: - continue - found = search(r"!\[(\d\d.\d\d)% przełącznika języków]", readme_content) - if not found: - found = search(r"!\[(\d+.\d\d)% language switchera]", readme_content) - if not found: - found = search(r"!\[(\d+.\d\d)% do language switchera]", readme_content) - if not found: - print(readme_content) - continue - number = float(found.group(1)) - progress.append(number) - dates.append(datetime.fromtimestamp(commit.authored_date)) - -pyplot.plot_date(dates, progress, linestyle="-", marker="") -pyplot.ylim(ymin=0) -pyplot.grid() -pyplot.gcf().autofmt_xdate() -pyplot.gca().yaxis.set_major_formatter(PercentFormatter()) -pyplot.title("Postęp tłumaczenia do dodania do przełącznika języków") -pyplot.savefig("language-switcher-progress.svg")