From 323ef7656e0a53dbb752fafe2491726f61254cc5 Mon Sep 17 00:00:00 2001 From: Chi-Sheng Liu Date: Mon, 23 Oct 2023 17:14:43 +0800 Subject: [PATCH 1/8] chore(environment): Update development environment settings --- .gitignore | 1 + poetry.lock | 105 ++++++++++++++++++++++++++++++++++++++++++++++--- pyproject.toml | 8 +++- 3 files changed, 106 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index d8f58c0..ade5121 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ *.sw[po] *.py[co] *.DS_Store +.idea dist/ diff --git a/poetry.lock b/poetry.lock index f6de599..b1e4236 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,8 +1,101 @@ -package = [] +# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. -[metadata] -lock-version = "1.1" -python-versions = "^3.6" -content-hash = "ae1f216c71b9b712a5c479d19bf075b718c35d9248fd89cb1eb7624528ec5ad1" +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.1.3" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.1.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"}, + {file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "packaging" +version = "23.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, + {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, +] + +[[package]] +name = "pluggy" +version = "1.3.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, + {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, +] -[metadata.files] +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pytest" +version = "7.4.2" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-7.4.2-py3-none-any.whl", hash = "sha256:1d881c6124e08ff0a1bb75ba3ec0bfd8b5354a01c194ddd5a0a870a48d99b002"}, + {file = "pytest-7.4.2.tar.gz", hash = "sha256:a766259cfab564a2ad52cb1aae1b881a75c3eb7e34ca3779697c23ed47c47069"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[metadata] +lock-version = "2.0" +python-versions = "^3.8" +content-hash = "0ef335fd8f9682f4239db86b12838ea153a2f617299b30d5f0e70fa6aafd3b8e" diff --git a/pyproject.toml b/pyproject.toml index 8e93ae5..a427f0d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,13 +5,14 @@ description = "" authors = ["PLM "] packages = [ - { include="src", from="." }, + { include = "src", from = "." }, ] [tool.poetry.dependencies] -python = "^3.6" +python = "^3.8" [tool.poetry.dev-dependencies] +pytest = "^7.4.2" [build-system] requires = ["poetry-core>=1.0.0"] @@ -19,3 +20,6 @@ build-backend = "poetry.core.masonry.api" [tool.poetry.scripts] missing = "src.missing:main" + +[tool.pytest.ini_options] +pythonpath = "src" From c1ec0f21922dbfaa348b9bac677ebb5cb873c323 Mon Sep 17 00:00:00 2001 From: Chi-Sheng Liu Date: Tue, 24 Oct 2023 10:59:34 +0800 Subject: [PATCH 2/8] refactor: Remove unused logics and use enum instead of string --- src/missing.py | 17 ++++++----------- tests/test_missing.py | 16 ++++++++-------- 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/src/missing.py b/src/missing.py index 465589f..9d85767 100644 --- a/src/missing.py +++ b/src/missing.py @@ -10,6 +10,7 @@ logger = logging.getLogger('missing') + class Mode(enum.Enum): ALL = 'all' OBEY_GITIGNORE = 'obey_gitignore' @@ -18,8 +19,9 @@ class Mode(enum.Enum): def __str__(self): return self.value + class Missing: - RE_TEST_PY = re.compile(r'^test.*?\.py$') + RE_TEST_PY = re.compile(r'^.*?\.py$') def __init__(self, mode, exclude): if not (isinstance(exclude, list) or isinstance(exclude, tuple)): @@ -29,23 +31,17 @@ def __init__(self, mode, exclude): logger.debug('Tracked files: %r', self._tracked_files) # append .git to exclude folder - exclude = set(['.git', *exclude]) + exclude = {'.git', *exclude} # normalize a pathname by collapsing redundant separators self._exclude = [os.path.normpath(path) for path in exclude] def run(self) -> int: - fail = 0 - - if self._find_for_unittest(): - fail = 1 - - return fail + return int(self._find_for_unittest()) @lru_cache(maxsize=None) def _check_init_py_exists(self, path: str) -> bool: """check the __init__.py exists on the current path and parent path""" - fail = False basedir = os.path.dirname(path) if basedir in ('', '.'): @@ -60,7 +56,7 @@ def _check_init_py_exists(self, path: str) -> bool: fail = True # create the __init__.py - with open(init_py, 'w') as fd: + with open(init_py, 'w'): logger.warning('create file: {}'.format(init_py)) return fail @@ -165,7 +161,6 @@ def main() -> int: '-q', '--quite', action='store_true', default=False, help='disable all log' ) - parser.add_argument( '-m', '--mode', diff --git a/tests/test_missing.py b/tests/test_missing.py index ca22b5e..53f6705 100644 --- a/tests/test_missing.py +++ b/tests/test_missing.py @@ -12,24 +12,24 @@ def temp_git_folder(): with tempfile.TemporaryDirectory() as tmp_dir: os.chdir(tmp_dir) - with open('.gitignore', 'w+') as fout: + with open('.gitignore', 'w') as fout: fout.write('test_ignore.py') os.mkdir('tests') os.mkdir(os.path.join('tests', 'ignored')) - with open(os.path.join('tests', 'ignored', 'test_ignore.py'), 'w+'): + with open(os.path.join('tests', 'ignored', 'test_ignore.py'), 'w'): pass os.mkdir(os.path.join('tests', 'untracked')) - with open(os.path.join('tests', 'untracked', 'test_untracked.py'), 'w+'): + with open(os.path.join('tests', 'untracked', 'test_untracked.py'), 'w'): pass os.mkdir(os.path.join('tests', 'staged')) - with open(os.path.join('tests', 'staged', 'test_staged.py'), 'w+'): + with open(os.path.join('tests', 'staged', 'test_staged.py'), 'w'): pass os.mkdir('data') - with open(os.path.join('data', 'user.xml'), 'w+') as fount: + with open(os.path.join('data', 'user.xml'), 'w'): pass os.system('git init') @@ -39,7 +39,7 @@ def temp_git_folder(): class TestMissing: def test__default_mode(self, temp_git_folder): - assert 1 == Missing(mode='all', exclude=[]).run() + assert 1 == Missing(mode=Mode.ALL, exclude=[]).run() assert not os.path.exists('data/__init__.py') assert os.path.isfile('tests/ignored/__init__.py') assert os.path.isfile('tests/untracked/__init__.py') @@ -48,7 +48,7 @@ def test__default_mode(self, temp_git_folder): assert 0 == Missing(mode=Mode.ALL, exclude=[]).run() def test__obey_gitignore(self, temp_git_folder): - assert 1 == Missing(mode='obey_gitignore', exclude=[]).run() + assert 1 == Missing(mode=Mode.OBEY_GITIGNORE, exclude=[]).run() assert not os.path.exists('data/__init__.py') assert not os.path.exists('tests/ignored/__init__.py') assert os.path.isfile('tests/untracked/__init__.py') @@ -57,7 +57,7 @@ def test__obey_gitignore(self, temp_git_folder): assert 0 == Missing(mode=Mode.OBEY_GITIGNORE, exclude=[]).run() def test__staged_only(self, temp_git_folder): - assert 1 == Missing(mode='staged_only', exclude=[]).run() + assert 1 == Missing(mode=Mode.STAGED_ONLY, exclude=[]).run() assert not os.path.exists('data/__init__.py') assert not os.path.exists('tests/ignored/__init__.py') assert not os.path.exists('tests/untracked/__init__.py') From 3b2e812f8d3cf5d97b8028cc07bda47fcd3b3e42 Mon Sep 17 00:00:00 2001 From: Chi-Sheng Liu Date: Tue, 24 Oct 2023 11:06:06 +0800 Subject: [PATCH 3/8] refactor(mode): Remove modes selection, always use obey_gitignore mode --- src/missing.py | 49 +++---------------------------------------- tests/test_missing.py | 26 ++++------------------- 2 files changed, 7 insertions(+), 68 deletions(-) diff --git a/src/missing.py b/src/missing.py index 9d85767..1db276c 100644 --- a/src/missing.py +++ b/src/missing.py @@ -1,6 +1,5 @@ #! /usr/bin/env python import argparse -import enum import logging import os import re @@ -11,23 +10,14 @@ logger = logging.getLogger('missing') -class Mode(enum.Enum): - ALL = 'all' - OBEY_GITIGNORE = 'obey_gitignore' - STAGED_ONLY = 'staged_only' - - def __str__(self): - return self.value - - class Missing: RE_TEST_PY = re.compile(r'^.*?\.py$') - def __init__(self, mode, exclude): + def __init__(self, exclude): if not (isinstance(exclude, list) or isinstance(exclude, tuple)): raise TypeError('exclude should be list or tuple') - self._tracked_files = self._find_tracked_files(mode) + self._tracked_files = self._list_files_obey_gitignore() logger.debug('Tracked files: %r', self._tracked_files) # append .git to exclude folder @@ -99,32 +89,6 @@ def _is_tracked(self, path): return True return path in self._tracked_files - def _find_tracked_files(self, mode): - if Mode(mode) == Mode.OBEY_GITIGNORE: - return self._list_files_obey_gitignore() - elif Mode(mode) == Mode.STAGED_ONLY: - return self._list_files_staged_only() - else: - return None - - def _list_files_staged_only(self): - # list staged files - tracked_files = ( - subprocess.check_output( - ['git', '--no-pager', 'diff', '--name-only', '--cached', '-z'], - encoding='utf-8', - ) - .rstrip('\0') - .split('\0') - ) - return set( - [ - os.path.normpath(tracked_file) - for tracked_file in tracked_files - if self.RE_TEST_PY.match(os.path.basename(tracked_file)) - ] - ) - def _list_files_obey_gitignore(self): # list committed files tracked_files = ( @@ -161,13 +125,6 @@ def main() -> int: '-q', '--quite', action='store_true', default=False, help='disable all log' ) - parser.add_argument( - '-m', - '--mode', - type=Mode, - default=Mode.ALL, - choices=list(Mode) - ) args = parser.parse_args() if args.quite: @@ -178,7 +135,7 @@ def main() -> int: handler = logging.StreamHandler() logger.addHandler(handler) - missing = Missing(args.mode, args.exclude or []) + missing = Missing(args.exclude or []) return missing.run() diff --git a/tests/test_missing.py b/tests/test_missing.py index 53f6705..468dc44 100644 --- a/tests/test_missing.py +++ b/tests/test_missing.py @@ -3,7 +3,7 @@ import pytest -from missing import Missing, Mode +from missing import Missing @pytest.fixture @@ -38,32 +38,14 @@ def temp_git_folder(): class TestMissing: - def test__default_mode(self, temp_git_folder): - assert 1 == Missing(mode=Mode.ALL, exclude=[]).run() - assert not os.path.exists('data/__init__.py') - assert os.path.isfile('tests/ignored/__init__.py') - assert os.path.isfile('tests/untracked/__init__.py') - assert os.path.isfile('tests/staged/__init__.py') - assert os.path.isfile('tests/__init__.py') - assert 0 == Missing(mode=Mode.ALL, exclude=[]).run() - def test__obey_gitignore(self, temp_git_folder): - assert 1 == Missing(mode=Mode.OBEY_GITIGNORE, exclude=[]).run() + assert 1 == Missing(exclude=[]).run() assert not os.path.exists('data/__init__.py') assert not os.path.exists('tests/ignored/__init__.py') assert os.path.isfile('tests/untracked/__init__.py') assert os.path.isfile('tests/staged/__init__.py') assert os.path.isfile('tests/__init__.py') - assert 0 == Missing(mode=Mode.OBEY_GITIGNORE, exclude=[]).run() - - def test__staged_only(self, temp_git_folder): - assert 1 == Missing(mode=Mode.STAGED_ONLY, exclude=[]).run() - assert not os.path.exists('data/__init__.py') - assert not os.path.exists('tests/ignored/__init__.py') - assert not os.path.exists('tests/untracked/__init__.py') - assert os.path.isfile('tests/staged/__init__.py') - assert os.path.isfile('tests/__init__.py') - assert 0 == Missing(mode=Mode.STAGED_ONLY, exclude=[]).run() + assert 0 == Missing(exclude=[]).run() def test__exclude(self, temp_git_folder): - assert 0 == Missing(mode=Mode.ALL, exclude=['tests']).run() + assert 0 == Missing(exclude=['tests']).run() From 94e8c570d14e64a86ee155d584d312bef0df6d0a Mon Sep 17 00:00:00 2001 From: Chi-Sheng Liu Date: Tue, 24 Oct 2023 14:21:55 +0800 Subject: [PATCH 4/8] test(unittest): Rewrite unittests to meet new requirements --- tests/test_missing.py | 158 +++++++++++++++++++++++++++++++++++------- 1 file changed, 132 insertions(+), 26 deletions(-) diff --git a/tests/test_missing.py b/tests/test_missing.py index 468dc44..2253df7 100644 --- a/tests/test_missing.py +++ b/tests/test_missing.py @@ -1,51 +1,157 @@ +from __future__ import annotations import os +import subprocess import tempfile +import shutil +from pathlib import Path import pytest from missing import Missing +class File: + def __init__(self, name: str): + self.name = name + + +class Dir: + def __init__(self, name: str = ".", children: list[Dir | File] | None = None): + self.name = name + self.children: list[Dir | File] = children or [] + + +def create_directory_tree(directory: Dir): + for child in directory.children: + if isinstance(child, Dir): + os.mkdir(child.name) + os.chdir(child.name) + create_directory_tree(child) + os.chdir('..') + else: + with open(child.name, 'w'): + pass + + @pytest.fixture def temp_git_folder(): - # Create a temp git folder so we could conduct the integration test + # Create a temp git folder so that we could conduct the integration test with tempfile.TemporaryDirectory() as tmp_dir: os.chdir(tmp_dir) with open('.gitignore', 'w') as fout: - fout.write('test_ignore.py') - - os.mkdir('tests') - os.mkdir(os.path.join('tests', 'ignored')) - with open(os.path.join('tests', 'ignored', 'test_ignore.py'), 'w'): - pass - - os.mkdir(os.path.join('tests', 'untracked')) - with open(os.path.join('tests', 'untracked', 'test_untracked.py'), 'w'): - pass + lines = [ + "ignored.py", + "ignored_folder/", + ".hidden_ignored_folder/", + ] + fout.write("\n".join(lines)) - os.mkdir(os.path.join('tests', 'staged')) - with open(os.path.join('tests', 'staged', 'test_staged.py'), 'w'): - pass + tree = Dir( + children=[ + Dir("folder1", [ + Dir("folder1_1", [ + Dir("folder1_1_1", [ + File("folder1_1_1.py"), + ]), + Dir("folder1_1_2", [ + File("__init__.py"), + ]), + ]), + Dir("folder1_2", [ + File("folder1_2.py"), + ]), + Dir("folder_containing_only_ignored_files", [ + File("ignored.py"), + ]), + Dir("ignored_folder", [ + File("some_ignored_file.py"), + ]), + Dir(".hidden_ignored_folder", [ + File("some_ignored_file.py"), + ]), + Dir("empty_folder"), + File("folder1.py"), + ]), + Dir("folder2", [ + Dir("folder2_1", [ + File("__init__.py"), + ]), + Dir("folder2_2", [ + File("folder2_2.py"), + ]), + File("folder2.py"), + File("__init__.py"), + ]), + Dir("folder3", [ + File("other_languages.js"), + ]), + Dir("empty_folder"), + Dir("ignored_folder", [ + File("some_ignored_file.py"), + ]), + Dir(".hidden_ignored_folder", [ + File("some_ignored_file.py"), + ]), + File("toplevel.py"), + ] + ) - os.mkdir('data') - with open(os.path.join('data', 'user.xml'), 'w'): - pass + create_directory_tree(tree) - os.system('git init') - os.system('git add %s' % os.path.join('tests', 'staged')) + if shutil.which('tree'): + subprocess.run(["tree", "-a", "-F", "--dirsfirst"]) + subprocess.run(["git", "init"]) + subprocess.run(["git", "add", "-A"]) yield class TestMissing: - def test__obey_gitignore(self, temp_git_folder): + def test__default(self, temp_git_folder): assert 1 == Missing(exclude=[]).run() - assert not os.path.exists('data/__init__.py') - assert not os.path.exists('tests/ignored/__init__.py') - assert os.path.isfile('tests/untracked/__init__.py') - assert os.path.isfile('tests/staged/__init__.py') - assert os.path.isfile('tests/__init__.py') + p = Path(".") + assert not (p / "__init__.py").exists() + assert not (p / "folder1" / "folder1_1" / "__init__.py").exists() + assert not (p / "ignored_folder" / "__init__.py").exists() + assert not (p / ".hidden_ignored_folder" / "__init__.py").exists() + assert not (p / "empty_folder" / "__init__.py").exists() + assert not (p / "folder1" / "ignored_folder" / "__init__.py").exists() + assert not (p / "folder1" / ".hidden_ignored_folder" / "__init__.py").exists() + assert not (p / "folder1" / "empty_folder" / "__init__.py").exists() + assert not (p / "folder3" / "__init__.py").exists() + assert not (p / "folder1" / "folder_containing_only_ignored_files" / "__init__.py").exists() + assert (p / "folder1" / "__init__.py").is_file() + assert (p / "folder1" / "folder1_1" / "folder1_1_1" / "__init__.py").is_file() + assert (p / "folder1" / "folder1_2" / "__init__.py").is_file() + assert (p / "folder2" / "folder2_2" / "__init__.py").is_file() assert 0 == Missing(exclude=[]).run() def test__exclude(self, temp_git_folder): - assert 0 == Missing(exclude=['tests']).run() + exclude = [ + os.path.join("folder1", "folder1_1"), + ] + assert 1 == Missing(exclude=exclude).run() + p = Path(".") + assert not (p / "__init__.py").exists() + assert not (p / "folder1" / "folder1_1" / "__init__.py").exists() + assert not (p / "ignored_folder" / "__init__.py").exists() + assert not (p / ".hidden_ignored_folder" / "__init__.py").exists() + assert not (p / "empty_folder" / "__init__.py").exists() + assert not (p / "folder1" / "ignored_folder" / "__init__.py").exists() + assert not (p / "folder1" / ".hidden_ignored_folder" / "__init__.py").exists() + assert not (p / "folder1" / "empty_folder" / "__init__.py").exists() + assert not (p / "folder3" / "__init__.py").exists() + assert not (p / "folder1" / "folder_containing_only_ignored_files" / "__init__.py").exists() + assert not (p / "folder1" / "folder1_1" / "folder1_1_1" / "__init__.py").exists() + assert (p / "folder1" / "__init__.py").is_file() + assert (p / "folder1" / "folder1_2" / "__init__.py").is_file() + assert (p / "folder2" / "folder2_2" / "__init__.py").is_file() + assert 0 == Missing(exclude=exclude).run() + + def test__non_str_sequence_exclude_throws_typeerror(self, temp_git_folder): + with pytest.raises(TypeError): + Missing(exclude=1) + + def test__non_dir_exclude_throws_typeerror(self, temp_git_folder): + with pytest.raises(TypeError): + Missing(exclude=['toplevel.py']) From 069da65a8a23d788e9fa3c8ec6b9e6e07dd0adf3 Mon Sep 17 00:00:00 2001 From: Chi-Sheng Liu Date: Tue, 24 Oct 2023 16:14:58 +0800 Subject: [PATCH 5/8] feat(missing): Write missing class logic to detect all __init__.py [FIS-1417] [FIS-1418] --- src/missing.py | 132 +++++++++++++++++-------------------------------- 1 file changed, 45 insertions(+), 87 deletions(-) diff --git a/src/missing.py b/src/missing.py index 1db276c..d1c6f39 100644 --- a/src/missing.py +++ b/src/missing.py @@ -4,104 +4,63 @@ import os import re import subprocess -from functools import lru_cache -from typing import Optional +from typing import Sequence logger = logging.getLogger('missing') class Missing: - RE_TEST_PY = re.compile(r'^.*?\.py$') - def __init__(self, exclude): + def __init__(self, exclude: Sequence[str]): if not (isinstance(exclude, list) or isinstance(exclude, tuple)): raise TypeError('exclude should be list or tuple') + for path in exclude: + if os.path.exists(path) and not os.path.isdir(path): + raise TypeError(f"exclude should only contain directories, found file {path}") - self._tracked_files = self._list_files_obey_gitignore() - logger.debug('Tracked files: %r', self._tracked_files) + files = [os.path.normpath(path) for path in self._list_files()] + logger.debug(f"{files=}") - # append .git to exclude folder - exclude = {'.git', *exclude} + exclude = [os.path.normpath(path) for path in exclude] + logger.debug(f"{exclude=}") - # normalize a pathname by collapsing redundant separators - self._exclude = [os.path.normpath(path) for path in exclude] + files_included = set() + for file in files: + if all([not file.startswith(e) for e in exclude]): + files_included.add(file) + logger.debug(f"{files_included=}") + + self.files = files_included def run(self) -> int: - return int(self._find_for_unittest()) - - @lru_cache(maxsize=None) - def _check_init_py_exists(self, path: str) -> bool: - """check the __init__.py exists on the current path and parent path""" - basedir = os.path.dirname(path) - - if basedir in ('', '.'): - return False - - # check the parent folder - fail = self._check_init_py_exists(basedir) - - # check the current __init__ exists or not - init_py = f'{basedir}/__init__.py' - if not os.path.exists(init_py): - fail = True - - # create the __init__.py - with open(init_py, 'w'): - logger.warning('create file: {}'.format(init_py)) - return fail - - def _find_for_unittest(self, basedir: Optional[str] = None) -> bool: - """the unittest find the test*.py only for the regular package""" - fail = False - init_run = False - - if basedir is None: - basedir = '.' - init_run = True - - for filename in os.listdir(basedir): - path = os.path.normpath(f'{basedir}/{filename}') - if path in self._exclude: - # skip the explicitly excluded path - logger.debug('exclude: %r', path) + """Return 1 if there are missing files, otherwise 0""" + is_failed = 0 + + for file in self.files: + basedir = os.path.dirname(file) + + if basedir in ('', '.'): continue - if os.path.isdir(path): - if self._find_for_unittest(path): - fail = True - elif self.RE_TEST_PY.match(filename): - if self._is_tracked(path): - logger.debug('check: %r', path) - if self._check_init_py_exists(path): - fail = True - else: - logger.debug('untracked: %r', path) - else: - logger.debug('no_match: %r', path) - - if init_run and fail: - logger.warning('found missing __init__.py for unittest') - - return fail - - def _is_tracked(self, path): - if self._tracked_files is None: - return True - return path in self._tracked_files - - def _list_files_obey_gitignore(self): - # list committed files - tracked_files = ( - subprocess.check_output( - ['git', 'ls-files', '-z'], - encoding='utf-8', - ) - .rstrip('\0') - .split('\0') - ) + init_py = os.path.join(basedir, '__init__.py') + if not os.path.exists(init_py): + is_failed = True + # create the __init__.py + with open(init_py, 'w'): + logger.warning(f'create file: {init_py}') + + return is_failed - # list untracked files - tracked_files += ( + @staticmethod + def _list_files(): + pattern = re.compile(r'^.*?\.py$') + + files = subprocess.check_output( + ['git', 'ls-files', '-z'], + encoding='utf-8', + ).rstrip('\0').split('\0') + + files += ( subprocess.check_output( ['git', 'ls-files', '-o', '--exclude-standard', '-z'], encoding='utf-8', @@ -109,12 +68,11 @@ def _list_files_obey_gitignore(self): .rstrip('\0') .split('\0') ) + return set( - [ - os.path.normpath(tracked_file) - for tracked_file in tracked_files - if self.RE_TEST_PY.match(os.path.basename(tracked_file)) - ] + os.path.normpath(file) + for file in files + if pattern.match(os.path.basename(file)) ) From 9a7aab3f57321b1f4ba83005b29e201d461da71b Mon Sep 17 00:00:00 2001 From: Chi-Sheng Liu Date: Tue, 24 Oct 2023 16:20:18 +0800 Subject: [PATCH 6/8] chore(version): Update version to 0.3.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a427f0d..ad83ef4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "missing" -version = "0.2.5" +version = "0.3.0" description = "" authors = ["PLM "] From b250cc2a46067a4e93ecd37a2a511e1c8fd226c4 Mon Sep 17 00:00:00 2001 From: Chi-Sheng Liu Date: Tue, 24 Oct 2023 17:42:13 +0800 Subject: [PATCH 7/8] ci(github-actions): Update python version in GitHub actions --- .github/workflows/lint.yml | 8 +++++--- .github/workflows/ut.yml | 8 ++++---- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 162d624..c16d7e2 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,15 +1,17 @@ name: lint -on: [pull_request] +on: [ pull_request ] jobs: pre-commit: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: fetch-depth: 1024 - - uses: actions/setup-python@v2 + - uses: actions/setup-python@v4 + with: + python-version: '3.8' - name: pre-commit env: BASE_SHA: ${{ github.event.pull_request.base.sha}} diff --git a/.github/workflows/ut.yml b/.github/workflows/ut.yml index 7891759..81138a3 100644 --- a/.github/workflows/ut.yml +++ b/.github/workflows/ut.yml @@ -1,16 +1,16 @@ name: Run Unit Test -on: [pull_request] +on: [ pull_request ] jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 with: - python-version: "3.6.8" + python-version: '3.8' - name: Install dependencies run: | pip install pip --upgrade From d674f50d20816f919d7e8ab3191c09857e9777d2 Mon Sep 17 00:00:00 2001 From: Chi-Sheng Liu Date: Tue, 24 Oct 2023 18:04:20 +0800 Subject: [PATCH 8/8] chore(pre-commit): Fix pre-commit error --- .pre-commit-config.yaml | 2 +- poetry.lock | 208 +++++++++++++++++++++++++++++++++++++- pyproject.toml | 1 + src/missing.py | 23 +++-- tests/test_missing.py | 219 ++++++++++++++++++++++++---------------- 5 files changed, 357 insertions(+), 96 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index be6433e..cfa733e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,6 +20,6 @@ repos: - --remove-unused-variables - --remove-all-unused-imports - repo: https://github.com/pycqa/isort - rev: 5.10.1 + rev: 5.12.0 hooks: - id: isort diff --git a/poetry.lock b/poetry.lock index b1e4236..c06fa29 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,16 @@ # This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. +[[package]] +name = "cfgv" +version = "3.4.0" +description = "Validate configuration and produce human readable error messages." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, + {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, +] + [[package]] name = "colorama" version = "0.4.6" @@ -11,6 +22,17 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "distlib" +version = "0.3.7" +description = "Distribution utilities" +optional = false +python-versions = "*" +files = [ + {file = "distlib-0.3.7-py2.py3-none-any.whl", hash = "sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057"}, + {file = "distlib-0.3.7.tar.gz", hash = "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8"}, +] + [[package]] name = "exceptiongroup" version = "1.1.3" @@ -25,6 +47,36 @@ files = [ [package.extras] test = ["pytest (>=6)"] +[[package]] +name = "filelock" +version = "3.12.4" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.8" +files = [ + {file = "filelock-3.12.4-py3-none-any.whl", hash = "sha256:08c21d87ded6e2b9da6728c3dff51baf1dcecf973b768ef35bcbc3447edb9ad4"}, + {file = "filelock-3.12.4.tar.gz", hash = "sha256:2e6f249f1f3654291606e046b09f1fd5eac39b360664c27f5aad072012f8bcbd"}, +] + +[package.extras] +docs = ["furo (>=2023.7.26)", "sphinx (>=7.1.2)", "sphinx-autodoc-typehints (>=1.24)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3)", "diff-cover (>=7.7)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)", "pytest-timeout (>=2.1)"] +typing = ["typing-extensions (>=4.7.1)"] + +[[package]] +name = "identify" +version = "2.5.30" +description = "File identification library for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "identify-2.5.30-py2.py3-none-any.whl", hash = "sha256:afe67f26ae29bab007ec21b03d4114f41316ab9dd15aa8736a167481e108da54"}, + {file = "identify-2.5.30.tar.gz", hash = "sha256:f302a4256a15c849b91cfcdcec052a8ce914634b2f77ae87dad29cd749f2d88d"}, +] + +[package.extras] +license = ["ukkonen"] + [[package]] name = "iniconfig" version = "2.0.0" @@ -36,6 +88,20 @@ files = [ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] +[[package]] +name = "nodeenv" +version = "1.8.0" +description = "Node.js virtual environment builder" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" +files = [ + {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, + {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, +] + +[package.dependencies] +setuptools = "*" + [[package]] name = "packaging" version = "23.2" @@ -47,6 +113,21 @@ files = [ {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, ] +[[package]] +name = "platformdirs" +version = "3.11.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +optional = false +python-versions = ">=3.7" +files = [ + {file = "platformdirs-3.11.0-py3-none-any.whl", hash = "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e"}, + {file = "platformdirs-3.11.0.tar.gz", hash = "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3"}, +] + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] + [[package]] name = "pluggy" version = "1.3.0" @@ -62,6 +143,25 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "pre-commit" +version = "2.14.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +optional = false +python-versions = ">=3.6.1" +files = [ + {file = "pre_commit-2.14.0-py2.py3-none-any.whl", hash = "sha256:ec3045ae62e1aa2eecfb8e86fa3025c2e3698f77394ef8d2011ce0aedd85b2d4"}, + {file = "pre_commit-2.14.0.tar.gz", hash = "sha256:2386eeb4cf6633712c7cc9ede83684d53c8cafca6b59f79c738098b51c6d206c"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +toml = "*" +virtualenv = ">=20.0.8" + [[package]] name = "pytest" version = "7.4.2" @@ -84,6 +184,92 @@ tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +[[package]] +name = "pyyaml" +version = "6.0.1" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, + {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, +] + +[[package]] +name = "setuptools" +version = "68.2.2" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "setuptools-68.2.2-py3-none-any.whl", hash = "sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a"}, + {file = "setuptools-68.2.2.tar.gz", hash = "sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] + [[package]] name = "tomli" version = "2.0.1" @@ -95,7 +281,27 @@ files = [ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] +[[package]] +name = "virtualenv" +version = "20.24.6" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.7" +files = [ + {file = "virtualenv-20.24.6-py3-none-any.whl", hash = "sha256:520d056652454c5098a00c0f073611ccbea4c79089331f60bf9d7ba247bb7381"}, + {file = "virtualenv-20.24.6.tar.gz", hash = "sha256:02ece4f56fbf939dbbc33c0715159951d6bf14aaf5457b092e4548e1382455af"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<4" + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] + [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "0ef335fd8f9682f4239db86b12838ea153a2f617299b30d5f0e70fa6aafd3b8e" +content-hash = "c2a682bb87e51216ebc6faf52e0b999522685e513b0f0e940f2644a56d9581da" diff --git a/pyproject.toml b/pyproject.toml index ad83ef4..1c386bb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,7 @@ python = "^3.8" [tool.poetry.dev-dependencies] pytest = "^7.4.2" +pre-commit = "2.14.0" [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/src/missing.py b/src/missing.py index d1c6f39..f99df89 100644 --- a/src/missing.py +++ b/src/missing.py @@ -10,25 +10,26 @@ class Missing: - def __init__(self, exclude: Sequence[str]): if not (isinstance(exclude, list) or isinstance(exclude, tuple)): raise TypeError('exclude should be list or tuple') for path in exclude: if os.path.exists(path) and not os.path.isdir(path): - raise TypeError(f"exclude should only contain directories, found file {path}") + raise TypeError( + f'exclude should only contain directories, found file {path}' + ) files = [os.path.normpath(path) for path in self._list_files()] - logger.debug(f"{files=}") + logger.debug(f'{files=}') exclude = [os.path.normpath(path) for path in exclude] - logger.debug(f"{exclude=}") + logger.debug(f'{exclude=}') files_included = set() for file in files: if all([not file.startswith(e) for e in exclude]): files_included.add(file) - logger.debug(f"{files_included=}") + logger.debug(f'{files_included=}') self.files = files_included @@ -55,10 +56,14 @@ def run(self) -> int: def _list_files(): pattern = re.compile(r'^.*?\.py$') - files = subprocess.check_output( - ['git', 'ls-files', '-z'], - encoding='utf-8', - ).rstrip('\0').split('\0') + files = ( + subprocess.check_output( + ['git', 'ls-files', '-z'], + encoding='utf-8', + ) + .rstrip('\0') + .split('\0') + ) files += ( subprocess.check_output( diff --git a/tests/test_missing.py b/tests/test_missing.py index 2253df7..ba6da92 100644 --- a/tests/test_missing.py +++ b/tests/test_missing.py @@ -1,8 +1,9 @@ from __future__ import annotations + import os +import shutil import subprocess import tempfile -import shutil from pathlib import Path import pytest @@ -16,7 +17,7 @@ def __init__(self, name: str): class Dir: - def __init__(self, name: str = ".", children: list[Dir | File] | None = None): + def __init__(self, name: str = '.', children: list[Dir | File] | None = None): self.name = name self.children: list[Dir | File] = children or [] @@ -41,111 +42,159 @@ def temp_git_folder(): os.chdir(tmp_dir) with open('.gitignore', 'w') as fout: lines = [ - "ignored.py", - "ignored_folder/", - ".hidden_ignored_folder/", + 'ignored.py', + 'ignored_folder/', + '.hidden_ignored_folder/', ] - fout.write("\n".join(lines)) + fout.write('\n'.join(lines)) tree = Dir( children=[ - Dir("folder1", [ - Dir("folder1_1", [ - Dir("folder1_1_1", [ - File("folder1_1_1.py"), - ]), - Dir("folder1_1_2", [ - File("__init__.py"), - ]), - ]), - Dir("folder1_2", [ - File("folder1_2.py"), - ]), - Dir("folder_containing_only_ignored_files", [ - File("ignored.py"), - ]), - Dir("ignored_folder", [ - File("some_ignored_file.py"), - ]), - Dir(".hidden_ignored_folder", [ - File("some_ignored_file.py"), - ]), - Dir("empty_folder"), - File("folder1.py"), - ]), - Dir("folder2", [ - Dir("folder2_1", [ - File("__init__.py"), - ]), - Dir("folder2_2", [ - File("folder2_2.py"), - ]), - File("folder2.py"), - File("__init__.py"), - ]), - Dir("folder3", [ - File("other_languages.js"), - ]), - Dir("empty_folder"), - Dir("ignored_folder", [ - File("some_ignored_file.py"), - ]), - Dir(".hidden_ignored_folder", [ - File("some_ignored_file.py"), - ]), - File("toplevel.py"), + Dir( + 'folder1', + [ + Dir( + 'folder1_1', + [ + Dir( + 'folder1_1_1', + [ + File('folder1_1_1.py'), + ], + ), + Dir( + 'folder1_1_2', + [ + File('__init__.py'), + ], + ), + ], + ), + Dir( + 'folder1_2', + [ + File('folder1_2.py'), + ], + ), + Dir( + 'folder_containing_only_ignored_files', + [ + File('ignored.py'), + ], + ), + Dir( + 'ignored_folder', + [ + File('some_ignored_file.py'), + ], + ), + Dir( + '.hidden_ignored_folder', + [ + File('some_ignored_file.py'), + ], + ), + Dir('empty_folder'), + File('folder1.py'), + ], + ), + Dir( + 'folder2', + [ + Dir( + 'folder2_1', + [ + File('__init__.py'), + ], + ), + Dir( + 'folder2_2', + [ + File('folder2_2.py'), + ], + ), + File('folder2.py'), + File('__init__.py'), + ], + ), + Dir( + 'folder3', + [ + File('other_languages.js'), + ], + ), + Dir('empty_folder'), + Dir( + 'ignored_folder', + [ + File('some_ignored_file.py'), + ], + ), + Dir( + '.hidden_ignored_folder', + [ + File('some_ignored_file.py'), + ], + ), + File('toplevel.py'), ] ) create_directory_tree(tree) if shutil.which('tree'): - subprocess.run(["tree", "-a", "-F", "--dirsfirst"]) - subprocess.run(["git", "init"]) - subprocess.run(["git", "add", "-A"]) + subprocess.run(['tree', '-a', '-F', '--dirsfirst']) + subprocess.run(['git', 'init']) + subprocess.run(['git', 'add', '-A']) yield class TestMissing: def test__default(self, temp_git_folder): assert 1 == Missing(exclude=[]).run() - p = Path(".") - assert not (p / "__init__.py").exists() - assert not (p / "folder1" / "folder1_1" / "__init__.py").exists() - assert not (p / "ignored_folder" / "__init__.py").exists() - assert not (p / ".hidden_ignored_folder" / "__init__.py").exists() - assert not (p / "empty_folder" / "__init__.py").exists() - assert not (p / "folder1" / "ignored_folder" / "__init__.py").exists() - assert not (p / "folder1" / ".hidden_ignored_folder" / "__init__.py").exists() - assert not (p / "folder1" / "empty_folder" / "__init__.py").exists() - assert not (p / "folder3" / "__init__.py").exists() - assert not (p / "folder1" / "folder_containing_only_ignored_files" / "__init__.py").exists() - assert (p / "folder1" / "__init__.py").is_file() - assert (p / "folder1" / "folder1_1" / "folder1_1_1" / "__init__.py").is_file() - assert (p / "folder1" / "folder1_2" / "__init__.py").is_file() - assert (p / "folder2" / "folder2_2" / "__init__.py").is_file() + p = Path('.') + assert not (p / '__init__.py').exists() + assert not (p / 'folder1' / 'folder1_1' / '__init__.py').exists() + assert not (p / 'ignored_folder' / '__init__.py').exists() + assert not (p / '.hidden_ignored_folder' / '__init__.py').exists() + assert not (p / 'empty_folder' / '__init__.py').exists() + assert not (p / 'folder1' / 'ignored_folder' / '__init__.py').exists() + assert not (p / 'folder1' / '.hidden_ignored_folder' / '__init__.py').exists() + assert not (p / 'folder1' / 'empty_folder' / '__init__.py').exists() + assert not (p / 'folder3' / '__init__.py').exists() + assert not ( + p / 'folder1' / 'folder_containing_only_ignored_files' / '__init__.py' + ).exists() + assert (p / 'folder1' / '__init__.py').is_file() + assert (p / 'folder1' / 'folder1_1' / 'folder1_1_1' / '__init__.py').is_file() + assert (p / 'folder1' / 'folder1_2' / '__init__.py').is_file() + assert (p / 'folder2' / 'folder2_2' / '__init__.py').is_file() assert 0 == Missing(exclude=[]).run() def test__exclude(self, temp_git_folder): exclude = [ - os.path.join("folder1", "folder1_1"), + os.path.join('folder1', 'folder1_1'), ] assert 1 == Missing(exclude=exclude).run() - p = Path(".") - assert not (p / "__init__.py").exists() - assert not (p / "folder1" / "folder1_1" / "__init__.py").exists() - assert not (p / "ignored_folder" / "__init__.py").exists() - assert not (p / ".hidden_ignored_folder" / "__init__.py").exists() - assert not (p / "empty_folder" / "__init__.py").exists() - assert not (p / "folder1" / "ignored_folder" / "__init__.py").exists() - assert not (p / "folder1" / ".hidden_ignored_folder" / "__init__.py").exists() - assert not (p / "folder1" / "empty_folder" / "__init__.py").exists() - assert not (p / "folder3" / "__init__.py").exists() - assert not (p / "folder1" / "folder_containing_only_ignored_files" / "__init__.py").exists() - assert not (p / "folder1" / "folder1_1" / "folder1_1_1" / "__init__.py").exists() - assert (p / "folder1" / "__init__.py").is_file() - assert (p / "folder1" / "folder1_2" / "__init__.py").is_file() - assert (p / "folder2" / "folder2_2" / "__init__.py").is_file() + p = Path('.') + assert not (p / '__init__.py').exists() + assert not (p / 'folder1' / 'folder1_1' / '__init__.py').exists() + assert not (p / 'ignored_folder' / '__init__.py').exists() + assert not (p / '.hidden_ignored_folder' / '__init__.py').exists() + assert not (p / 'empty_folder' / '__init__.py').exists() + assert not (p / 'folder1' / 'ignored_folder' / '__init__.py').exists() + assert not (p / 'folder1' / '.hidden_ignored_folder' / '__init__.py').exists() + assert not (p / 'folder1' / 'empty_folder' / '__init__.py').exists() + assert not (p / 'folder3' / '__init__.py').exists() + assert not ( + p / 'folder1' / 'folder_containing_only_ignored_files' / '__init__.py' + ).exists() + assert not ( + p / 'folder1' / 'folder1_1' / 'folder1_1_1' / '__init__.py' + ).exists() + assert (p / 'folder1' / '__init__.py').is_file() + assert (p / 'folder1' / 'folder1_2' / '__init__.py').is_file() + assert (p / 'folder2' / 'folder2_2' / '__init__.py').is_file() assert 0 == Missing(exclude=exclude).run() def test__non_str_sequence_exclude_throws_typeerror(self, temp_git_folder):