diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 17fde83da5..046603a313 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -21,7 +21,7 @@ jobs: max-parallel: 15 matrix: node: [16.x, 18.x, 20.x] - python: ["3.8", "3.11"] + python: ["3.8", "3.11", "3.12"] os: [macos-latest, ubuntu-latest, windows-latest] runs-on: ${{ matrix.os }} steps: @@ -52,5 +52,11 @@ jobs: # run: python -m pytest --doctest-modules - name: Environment Information run: npx envinfo - - name: Run Node tests - run: npm test + - name: Run Node tests (macOS or Linux) + if: runner.os != 'Windows' + shell: bash + run: npm test --python="${pythonLocation}/python" + - name: Run tests (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: npm run test --python="${env:pythonLocation}\\python.exe" diff --git a/.github/workflows/visual-studio.yml b/.github/workflows/visual-studio.yml index 12125e5447..96a9075d55 100644 --- a/.github/workflows/visual-studio.yml +++ b/.github/workflows/visual-studio.yml @@ -12,8 +12,11 @@ jobs: fail-fast: false max-parallel: 8 matrix: - os: [windows-latest] - msvs-version: [2016, 2019, 2022] # https://github.com/actions/virtual-environments/tree/main/images/win + include: + - os: windows-2019 + msvs-verison: 2019 + - os: windows-2022 + msvs-version: 2022 runs-on: ${{ matrix.os }} steps: - name: Checkout Repository @@ -21,13 +24,10 @@ jobs: - name: Install Dependencies run: | npm install --no-progress - # npm audit fix --force - - name: Set Windows environment - if: startsWith(matrix.os, 'windows') - run: | - echo 'GYP_MSVS_VERSION=${{ matrix.msvs-version }}' >> $Env:GITHUB_ENV - echo 'GYP_MSVS_OVERRIDE_PATH=C:\\Dummy' >> $Env:GITHUB_ENV - name: Environment Information run: npx envinfo - name: Run Node tests - run: npm test + shell: pwsh + run: | + $pythonLocation = (Get-Command python).Source + npm run test --python="${pythonLocation}" --msvs-version="${{ matrix.msvs-version }}" diff --git a/gyp/.flake8 b/gyp/.flake8 deleted file mode 100644 index ea0c7680ef..0000000000 --- a/gyp/.flake8 +++ /dev/null @@ -1,4 +0,0 @@ -[flake8] -max-complexity = 101 -max-line-length = 88 -extend-ignore = E203 # whitespace before ':' to agree with psf/black diff --git a/gyp/.github/workflows/Python_tests.yml b/gyp/.github/workflows/Python_tests.yml index aad135027c..049d5fe50c 100644 --- a/gyp/.github/workflows/Python_tests.yml +++ b/gyp/.github/workflows/Python_tests.yml @@ -13,24 +13,25 @@ jobs: runs-on: ${{ matrix.os }} strategy: fail-fast: false - max-parallel: 8 + max-parallel: 5 matrix: os: [macos-latest, ubuntu-latest] # , windows-latest] - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11-dev"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} + allow-prereleases: true - name: Install dependencies run: | python -m pip install --upgrade pip setuptools pip install --editable ".[dev]" - run: ./gyp -V && ./gyp --version && gyp -V && gyp --version - - name: Lint with flake8 - run: flake8 . --ignore=E203,W503 --max-complexity=101 --max-line-length=88 --show-source --statistics - - name: Test with pytest + - name: Lint with ruff # See pyproject.toml for settings + run: ruff --output-format=github . + - name: Test with pytest # See pyproject.toml for settings run: pytest # - name: Run doctests with pytest # run: pytest --doctest-modules diff --git a/gyp/.github/workflows/node-gyp.yml b/gyp/.github/workflows/node-gyp.yml index 7cc1f9e075..ebe7497521 100644 --- a/gyp/.github/workflows/node-gyp.yml +++ b/gyp/.github/workflows/node-gyp.yml @@ -11,26 +11,33 @@ jobs: fail-fast: false matrix: os: [macos-latest, ubuntu-latest, windows-latest] - python: ["3.7", "3.10"] + python: ["3.8", "3.10", "3.12"] runs-on: ${{ matrix.os }} steps: - name: Clone gyp-next - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: path: gyp-next - name: Clone nodejs/node-gyp - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: nodejs/node-gyp path: node-gyp - uses: actions/setup-node@v3 with: - node-version: 14.x + node-version: 18.x - uses: actions/setup-python@v4 with: python-version: ${{ matrix.python }} - - name: Install dependencies + allow-prereleases: true + - name: Install Python dependencies + run: | + cd gyp-next + python -m pip install --upgrade pip setuptools + pip install --editable . + pip uninstall -y gyp-next + - name: Install Node.js dependencies run: | cd node-gyp npm install --no-progress @@ -39,7 +46,15 @@ jobs: run: | rm -rf node-gyp/gyp cp -r gyp-next node-gyp/gyp - - name: Run tests + - name: Run tests (macOS or Linux) + if: runner.os != 'Windows' + shell: bash + run: | + cd node-gyp + npm test --python="${pythonLocation}/python" + - name: Run tests (Windows) + if: runner.os == 'Windows' + shell: pwsh run: | cd node-gyp - npm test + npm run test --python="${env:pythonLocation}\\python.exe" diff --git a/gyp/.github/workflows/nodejs-windows.yml b/gyp/.github/workflows/nodejs-windows.yml index 4e6c9548ff..3f52ff9ce7 100644 --- a/gyp/.github/workflows/nodejs-windows.yml +++ b/gyp/.github/workflows/nodejs-windows.yml @@ -9,14 +9,14 @@ on: jobs: build-windows: - runs-on: windows-2019 + runs-on: windows-latest steps: - name: Clone gyp-next - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: path: gyp-next - name: Clone nodejs/node - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: nodejs/node path: node diff --git a/gyp/AUTHORS b/gyp/AUTHORS index f49a357b9e..c05d25b2cb 100644 --- a/gyp/AUTHORS +++ b/gyp/AUTHORS @@ -14,3 +14,4 @@ Tom Freudenberg
# pre-release + [-_\.]? + (?Palpha|a|beta|b|preview|pre|c|rc) + [-_\.]? + (?P [0-9]+)? + )? + (?P # post release + (?:-(?P [0-9]+)) + | + (?: + [-_\.]? + (?P post|rev|r) + [-_\.]? + (?P [0-9]+)? + ) + )? + (?P # dev release + [-_\.]? + (?P dev) + [-_\.]? + (?P [0-9]+)? + )? + ) + (?:\+(?P [a-z0-9]+(?:[-_\.][a-z0-9]+)*))? # local version +""" + +VERSION_PATTERN = _VERSION_PATTERN +""" +A string containing the regular expression used to match a valid version. + +The pattern is not anchored at either end, and is intended for embedding in larger +expressions (for example, matching a version number as part of a file name). The +regular expression should be compiled with the ``re.VERBOSE`` and ``re.IGNORECASE`` +flags set. + +:meta hide-value: +""" + + +class Version(_BaseVersion): + """This class abstracts handling of a project's versions. + + A :class:`Version` instance is comparison aware and can be compared and + sorted using the standard Python interfaces. + + >>> v1 = Version("1.0a5") + >>> v2 = Version("1.0") + >>> v1 + + >>> v2 + + >>> v1 < v2 + True + >>> v1 == v2 + False + >>> v1 > v2 + False + >>> v1 >= v2 + False + >>> v1 <= v2 + True + """ + + _regex = re.compile(r"^\s*" + VERSION_PATTERN + r"\s*$", re.VERBOSE | re.IGNORECASE) + _key: CmpKey + + def __init__(self, version: str) -> None: + """Initialize a Version object. + + :param version: + The string representation of a version which will be parsed and normalized + before use. + :raises InvalidVersion: + If the ``version`` does not conform to PEP 440 in any way then this + exception will be raised. + """ + + # Validate the version and parse it into pieces + match = self._regex.search(version) + if not match: + raise InvalidVersion(f"Invalid version: '{version}'") + + # Store the parsed out pieces of the version + self._version = _Version( + epoch=int(match.group("epoch")) if match.group("epoch") else 0, + release=tuple(int(i) for i in match.group("release").split(".")), + pre=_parse_letter_version(match.group("pre_l"), match.group("pre_n")), + post=_parse_letter_version( + match.group("post_l"), match.group("post_n1") or match.group("post_n2") + ), + dev=_parse_letter_version(match.group("dev_l"), match.group("dev_n")), + local=_parse_local_version(match.group("local")), + ) + + # Generate a key which will be used for sorting + self._key = _cmpkey( + self._version.epoch, + self._version.release, + self._version.pre, + self._version.post, + self._version.dev, + self._version.local, + ) + + def __repr__(self) -> str: + """A representation of the Version that shows all internal state. + + >>> Version('1.0.0') + + """ + return f" " + + def __str__(self) -> str: + """A string representation of the version that can be rounded-tripped. + + >>> str(Version("1.0a5")) + '1.0a5' + """ + parts = [] + + # Epoch + if self.epoch != 0: + parts.append(f"{self.epoch}!") + + # Release segment + parts.append(".".join(str(x) for x in self.release)) + + # Pre-release + if self.pre is not None: + parts.append("".join(str(x) for x in self.pre)) + + # Post-release + if self.post is not None: + parts.append(f".post{self.post}") + + # Development release + if self.dev is not None: + parts.append(f".dev{self.dev}") + + # Local version segment + if self.local is not None: + parts.append(f"+{self.local}") + + return "".join(parts) + + @property + def epoch(self) -> int: + """The epoch of the version. + + >>> Version("2.0.0").epoch + 0 + >>> Version("1!2.0.0").epoch + 1 + """ + return self._version.epoch + + @property + def release(self) -> Tuple[int, ...]: + """The components of the "release" segment of the version. + + >>> Version("1.2.3").release + (1, 2, 3) + >>> Version("2.0.0").release + (2, 0, 0) + >>> Version("1!2.0.0.post0").release + (2, 0, 0) + + Includes trailing zeroes but not the epoch or any pre-release / development / + post-release suffixes. + """ + return self._version.release + + @property + def pre(self) -> Optional[Tuple[str, int]]: + """The pre-release segment of the version. + + >>> print(Version("1.2.3").pre) + None + >>> Version("1.2.3a1").pre + ('a', 1) + >>> Version("1.2.3b1").pre + ('b', 1) + >>> Version("1.2.3rc1").pre + ('rc', 1) + """ + return self._version.pre + + @property + def post(self) -> Optional[int]: + """The post-release number of the version. + + >>> print(Version("1.2.3").post) + None + >>> Version("1.2.3.post1").post + 1 + """ + return self._version.post[1] if self._version.post else None + + @property + def dev(self) -> Optional[int]: + """The development number of the version. + + >>> print(Version("1.2.3").dev) + None + >>> Version("1.2.3.dev1").dev + 1 + """ + return self._version.dev[1] if self._version.dev else None + + @property + def local(self) -> Optional[str]: + """The local version segment of the version. + + >>> print(Version("1.2.3").local) + None + >>> Version("1.2.3+abc").local + 'abc' + """ + if self._version.local: + return ".".join(str(x) for x in self._version.local) + else: + return None + + @property + def public(self) -> str: + """The public portion of the version. + + >>> Version("1.2.3").public + '1.2.3' + >>> Version("1.2.3+abc").public + '1.2.3' + >>> Version("1.2.3+abc.dev1").public + '1.2.3' + """ + return str(self).split("+", 1)[0] + + @property + def base_version(self) -> str: + """The "base version" of the version. + + >>> Version("1.2.3").base_version + '1.2.3' + >>> Version("1.2.3+abc").base_version + '1.2.3' + >>> Version("1!1.2.3+abc.dev1").base_version + '1!1.2.3' + + The "base version" is the public version of the project without any pre or post + release markers. + """ + parts = [] + + # Epoch + if self.epoch != 0: + parts.append(f"{self.epoch}!") + + # Release segment + parts.append(".".join(str(x) for x in self.release)) + + return "".join(parts) + + @property + def is_prerelease(self) -> bool: + """Whether this version is a pre-release. + + >>> Version("1.2.3").is_prerelease + False + >>> Version("1.2.3a1").is_prerelease + True + >>> Version("1.2.3b1").is_prerelease + True + >>> Version("1.2.3rc1").is_prerelease + True + >>> Version("1.2.3dev1").is_prerelease + True + """ + return self.dev is not None or self.pre is not None + + @property + def is_postrelease(self) -> bool: + """Whether this version is a post-release. + + >>> Version("1.2.3").is_postrelease + False + >>> Version("1.2.3.post1").is_postrelease + True + """ + return self.post is not None + + @property + def is_devrelease(self) -> bool: + """Whether this version is a development release. + + >>> Version("1.2.3").is_devrelease + False + >>> Version("1.2.3.dev1").is_devrelease + True + """ + return self.dev is not None + + @property + def major(self) -> int: + """The first item of :attr:`release` or ``0`` if unavailable. + + >>> Version("1.2.3").major + 1 + """ + return self.release[0] if len(self.release) >= 1 else 0 + + @property + def minor(self) -> int: + """The second item of :attr:`release` or ``0`` if unavailable. + + >>> Version("1.2.3").minor + 2 + >>> Version("1").minor + 0 + """ + return self.release[1] if len(self.release) >= 2 else 0 + + @property + def micro(self) -> int: + """The third item of :attr:`release` or ``0`` if unavailable. + + >>> Version("1.2.3").micro + 3 + >>> Version("1").micro + 0 + """ + return self.release[2] if len(self.release) >= 3 else 0 + + +def _parse_letter_version( + letter: Optional[str], number: Union[str, bytes, SupportsInt, None] +) -> Optional[Tuple[str, int]]: + + if letter: + # We consider there to be an implicit 0 in a pre-release if there is + # not a numeral associated with it. + if number is None: + number = 0 + + # We normalize any letters to their lower case form + letter = letter.lower() + + # We consider some words to be alternate spellings of other words and + # in those cases we want to normalize the spellings to our preferred + # spelling. + if letter == "alpha": + letter = "a" + elif letter == "beta": + letter = "b" + elif letter in ["c", "pre", "preview"]: + letter = "rc" + elif letter in ["rev", "r"]: + letter = "post" + + return letter, int(number) + if not letter and number: + # We assume if we are given a number, but we are not given a letter + # then this is using the implicit post release syntax (e.g. 1.0-1) + letter = "post" + + return letter, int(number) + + return None + + +_local_version_separators = re.compile(r"[\._-]") + + +def _parse_local_version(local: Optional[str]) -> Optional[LocalType]: + """ + Takes a string like abc.1.twelve and turns it into ("abc", 1, "twelve"). + """ + if local is not None: + return tuple( + part.lower() if not part.isdigit() else int(part) + for part in _local_version_separators.split(local) + ) + return None + + +def _cmpkey( + epoch: int, + release: Tuple[int, ...], + pre: Optional[Tuple[str, int]], + post: Optional[Tuple[str, int]], + dev: Optional[Tuple[str, int]], + local: Optional[LocalType], +) -> CmpKey: + + # When we compare a release version, we want to compare it with all of the + # trailing zeros removed. So we'll use a reverse the list, drop all the now + # leading zeros until we come to something non zero, then take the rest + # re-reverse it back into the correct order and make it a tuple and use + # that for our sorting key. + _release = tuple( + reversed(list(itertools.dropwhile(lambda x: x == 0, reversed(release)))) + ) + + # We need to "trick" the sorting algorithm to put 1.0.dev0 before 1.0a0. + # We'll do this by abusing the pre segment, but we _only_ want to do this + # if there is not a pre or a post segment. If we have one of those then + # the normal sorting rules will handle this case correctly. + if pre is None and post is None and dev is not None: + _pre: CmpPrePostDevType = NegativeInfinity + # Versions without a pre-release (except as noted above) should sort after + # those with one. + elif pre is None: + _pre = Infinity + else: + _pre = pre + + # Versions without a post segment should sort before those with one. + if post is None: + _post: CmpPrePostDevType = NegativeInfinity + + else: + _post = post + + # Versions without a development segment should sort after those with one. + if dev is None: + _dev: CmpPrePostDevType = Infinity + + else: + _dev = dev + + if local is None: + # Versions without a local segment should sort before those with one. + _local: CmpLocalType = NegativeInfinity + else: + # Versions with a local segment need that segment parsed to implement + # the sorting rules in PEP440. + # - Alpha numeric segments sort before numeric segments + # - Alpha numeric segments sort lexicographically + # - Numeric segments sort numerically + # - Shorter versions sort before longer versions when the prefixes + # match exactly + _local = tuple( + (i, "") if isinstance(i, int) else (NegativeInfinity, i) for i in local + ) + + return epoch, _release, _pre, _post, _dev, _local diff --git a/gyp/pyproject.toml b/gyp/pyproject.toml index d8a5451520..0c25d0b3c1 100644 --- a/gyp/pyproject.toml +++ b/gyp/pyproject.toml @@ -4,14 +4,16 @@ build-backend = "setuptools.build_meta" [project] name = "gyp-next" -version = "0.14.0" +version = "0.16.1" authors = [ { name="Node.js contributors", email="ryzokuken@disroot.org" }, ] description = "A fork of the GYP build system for use in the Node.js projects" readme = "README.md" license = { file="LICENSE" } -requires-python = ">=3.6" +requires-python = ">=3.8" +# The Python module "packaging" is vendored in the "pylib/packaging" directory to support Python >= 3.12. +# dependencies = ["packaging>=23.1"] # Uncomment this line if the vendored version is removed. classifiers = [ "Development Status :: 3 - Alpha", "Environment :: Console", @@ -20,15 +22,14 @@ classifiers = [ "Natural Language :: English", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ] [project.optional-dependencies] -dev = ["flake8", "pytest"] +dev = ["flake8", "ruff", "pytest"] [project.scripts] gyp = "gyp:script_main" @@ -36,6 +37,83 @@ gyp = "gyp:script_main" [project.urls] "Homepage" = "https://github.com/nodejs/gyp-next" +[tool.ruff] +select = [ + "C4", # flake8-comprehensions + "C90", # McCabe cyclomatic complexity + "DTZ", # flake8-datetimez + "E", # pycodestyle + "F", # Pyflakes + "G", # flake8-logging-format + "ICN", # flake8-import-conventions + "INT", # flake8-gettext + "PL", # Pylint + "PYI", # flake8-pyi + "RSE", # flake8-raise + "RUF", # Ruff-specific rules + "T10", # flake8-debugger + "TCH", # flake8-type-checking + "TID", # flake8-tidy-imports + "UP", # pyupgrade + "W", # pycodestyle + "YTT", # flake8-2020 + # "A", # flake8-builtins + # "ANN", # flake8-annotations + # "ARG", # flake8-unused-arguments + # "B", # flake8-bugbear + # "BLE", # flake8-blind-except + # "COM", # flake8-commas + # "D", # pydocstyle + # "DJ", # flake8-django + # "EM", # flake8-errmsg + # "ERA", # eradicate + # "EXE", # flake8-executable + # "FBT", # flake8-boolean-trap + # "I", # isort + # "INP", # flake8-no-pep420 + # "ISC", # flake8-implicit-str-concat + # "N", # pep8-naming + # "NPY", # NumPy-specific rules + # "PD", # pandas-vet + # "PGH", # pygrep-hooks + # "PIE", # flake8-pie + # "PT", # flake8-pytest-style + # "PTH", # flake8-use-pathlib + # "Q", # flake8-quotes + # "RET", # flake8-return + # "S", # flake8-bandit + # "SIM", # flake8-simplify + # "SLF", # flake8-self + # "T20", # flake8-print + # "TRY", # tryceratops +] +ignore = [ + "E721", + "PLC1901", + "PLR0402", + "PLR1714", + "PLR2004", + "PLR5501", + "PLW0603", + "PLW2901", + "PYI024", + "RUF005", + "RUF012", + "UP031", +] +extend-exclude = ["pylib/packaging"] +line-length = 88 +target-version = "py37" + +[tool.ruff.mccabe] +max-complexity = 101 + +[tool.ruff.pylint] +max-args = 11 +max-branches = 108 +max-returns = 10 +max-statements = 286 + [tool.setuptools] package-dir = {"" = "pylib"} packages = ["gyp", "gyp.generator"] diff --git a/gyp/tools/pretty_sln.py b/gyp/tools/pretty_sln.py index 6ca0cd12a7..cf0638a23d 100755 --- a/gyp/tools/pretty_sln.py +++ b/gyp/tools/pretty_sln.py @@ -34,10 +34,10 @@ def BuildProject(project, built, projects, deps): def ParseSolution(solution_file): # All projects, their clsid and paths. - projects = dict() + projects = {} # A list of dependencies associated with a project. - dependencies = dict() + dependencies = {} # Regular expressions that matches the SLN format. # The first line of a project definition. diff --git a/gyp/tools/pretty_vcproj.py b/gyp/tools/pretty_vcproj.py index 00d32debda..72c65d7fc4 100755 --- a/gyp/tools/pretty_vcproj.py +++ b/gyp/tools/pretty_vcproj.py @@ -21,7 +21,7 @@ __author__ = "nsylvain (Nicolas Sylvain)" ARGUMENTS = None -REPLACEMENTS = dict() +REPLACEMENTS = {} def cmp(x, y):