diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0722a9a..5ac5eb1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,9 +16,9 @@ jobs: - name: Checkout working copy uses: actions/checkout@v3 - name: Set up Python - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: - python-version: "3.10" + python-version: "3.11" - name: Install checkers run: | python -mpip install --upgrade pip @@ -28,58 +28,52 @@ jobs: - name: black run: black --check --diff --color --quiet . + # REPLACE BY: job which python -mbuild, and uploads the sdist and wheel to artifacts + # build is not binary so can just build the one using whatever python version compile: - strategy: - fail-fast: false - matrix: - python-version: - - "2.7" - - "3.6" - - "3.7" - - "3.8" - - "3.9" - - "3.10" - - "3.11" - - "3.12.0-alpha - 3.12" - - "pypy-3.8" - - "pypy-3.9" - pyyaml-version: ["5.1.*", "5.4.*", "6.0.*", "6.*"] - include: - - python-version: 3.6 - os: ubuntu-20.04 - exclude: - - python-version: 2.7 - pyyaml-version: 6.0.* - - python-version: 2.7 - pyyaml-version: 6.* - runs-on: ${{ matrix.os || 'ubuntu-latest' }} + runs-on: ubuntu-latest steps: - name: Checkout working copy uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 with: - python-version: ${{ matrix.python-version }} + submodules: true + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.11" - name: Install dependency run: | python -mpip install --upgrade pip - python -mpip install pyyaml==${{ matrix.pyyaml-version }} - - name: Build regexes.py - run: python setup.py build_regexes -i - - name: Check results + python -mpip install build + - name: Build sdist and wheel run: | - # check that _regexes exists, and .eggs does not (== setuptools used our dependency) - test -e src/ua_parser/_regexes.py -a ! -e .eggs + python -mbuild + - name: Upload sdist + uses: actions/upload-artifact@v3 + with: + name: sdist + path: dist/*.tar.gz + retention-days: 1 + + - name: Upload wheel + uses: actions/upload-artifact@v3 + with: + name: wheel + path: dist/*.whl + retention-days: 1 test: + runs-on: ubuntu-latest + needs: compile strategy: fail-fast: false matrix: + source: + - wheel + - sdist + - source python-version: - - "2.7" - - "3.6" - - "3.7" - "3.8" - "3.9" - "3.10" @@ -88,17 +82,17 @@ jobs: - "pypy-3.8" - "pypy-3.9" include: - - python-version: 3.6 - os: ubuntu-20.04 - runs-on: ${{ matrix.os || 'ubuntu-latest' }} - + - source: sdist + artifact: dist/*.tar.gz + - source: wheel + artifact: dist/*.whl steps: - name: Checkout working copy uses: actions/checkout@v3 with: submodules: true - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install test dependencies @@ -112,13 +106,14 @@ jobs: fi fi python -mpip install -r requirements_dev.txt + - name: download ${{ matrix.source }} artifact + if: matrix.artifact + uses: actions/download-artifact@v3 + with: + name: ${{ matrix.source }} + path: dist/ - name: install package in environment - run: pip install . + run: | + pip install ${{ matrix.artifact || '.' }} - name: run tests - if: ${{ matrix.python-version != '2.7' }} run: pytest -v -Werror -Wignore::ImportWarning --doctest-glob="*.rst" - - name: run tests - # pprint formatting was widely altered in 3.5, so can't run - # doctests before then - if: ${{ matrix.python-version == '2.7' }} - run: pytest -v -Werror -Wignore::ImportWarning diff --git a/MANIFEST.in b/MANIFEST.in index a0600da..9c004de 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,9 @@ +exclude .* +prune .github +global-exclude *~ + include README.rst include LICENSE -global-exclude *~ +graft uap-core +exclude uap-core/.* +recursive-exclude uap-core *.js diff --git a/Makefile b/Makefile index ff691c4..0604a25 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ format: @black . release: clean - python setup.py sdist bdist_wheel + pyproject-build twine upload -s dist/* .PHONY: all test clean format release diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..48b6aa9 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,44 @@ +[build-system] +requires = ["setuptools", "setuptools-scm", "PyYaml"] +build-backend = "setuptools.build_meta" + +[project] +name = "ua-parser" +description = "Python port of Browserscope's user agent parser" +version = "1.0.0a" +readme = "README.rst" +requires-python = ">=3.8" +dependencies = [] +optional-dependencies = { yaml = ["PyYaml"] } + +license = {text = "Apache 2.0"} +urls = {repository = "https://github.com/ua-parser/uap-python"} + +authors = [ + { name = "Stephen Lamm", email = "slamm@google.com"}, + { name = "PBS", email = "no-reply@pbs.org" }, + { name = "Selwin Ong", email = "selwin.ong@gmail.com" }, + { name = "Matt Robenolt", email = "matt@ydekproductions.com" }, + { name = "Lindsey Simon", email = "lsimon@commoner.com" }, +] +maintainers = [ + { name = "masklinn", email = "uap@masklinn.net" } +] + +classifiers = [ + "Development Status :: 4 - Beta", + "Environment :: Web Environment", + "Intended Audience :: Developers", + "Operating System :: OS Independent", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Software Development :: Libraries :: Python Modules", + "Programming Language :: Python", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy" +] diff --git a/setup.cfg b/setup.cfg index 2a9acf1..9b07aee 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,8 @@ -[bdist_wheel] -universal = 1 +[options] +packages = find: +package_dir = + =src +setup_requires = pyyaml + +[options.packages.find] +where = src diff --git a/setup.py b/setup.py index 26aabf3..0e14118 100644 --- a/setup.py +++ b/setup.py @@ -1,68 +1,39 @@ #!/usr/bin/env python # flake8: noqa -import os -from distutils import log -from distutils.core import Command -from distutils.command.build import build as _build -from setuptools import setup, find_packages -from setuptools.command.develop import develop as _develop -from setuptools.command.sdist import sdist as _sdist -from setuptools.command.install import install as _install - - -def check_output(*args, **kwargs): - from subprocess import Popen - - proc = Popen(*args, **kwargs) - output, _ = proc.communicate() - rv = proc.poll() - assert rv == 0, output - - -class build_regexes(Command): - description = "build supporting regular expressions from uap-core" - user_options = [ - ("work-path=", "w", "The working directory for source files. Defaults to ."), - ("build-lib=", "b", "directory for script runtime modules"), - ( - "inplace", - "i", - "ignore build-lib and put compiled javascript files into the source " - + "directory alongside your pure Python modules", - ), - ( - "force", - "f", - "Force rebuilding of static content. Defaults to rebuilding on version " - "change detection.", - ), - ] - boolean_options = ["force"] - - def initialize_options(self): - self.build_lib = None - self.force = None - self.work_path = None - self.inplace = None - - def finalize_options(self): - install = self.distribution.get_command_obj("install") - sdist = self.distribution.get_command_obj("sdist") - build_ext = self.distribution.get_command_obj("build_ext") - - if self.inplace is None: - self.inplace = ( - (build_ext.inplace or install.finalized or sdist.finalized) and 1 or 0 - ) +from contextlib import suppress +from os import fspath +from pathlib import Path +from typing import Optional, List, Dict - if self.inplace: - self.build_lib = "./src" - else: - self.set_undefined_options("build", ("build_lib", "build_lib")) - if self.work_path is None: - self.work_path = os.path.realpath(os.path.join(os.path.dirname(__file__))) +from setuptools import setup, Command, find_namespace_packages +from setuptools.command.build import build, SubCommand +from setuptools.command.editable_wheel import editable_wheel + +import yaml + + +build.sub_commands.insert(0, ("compile-regexes", None)) + + +class CompileRegexes(Command, SubCommand): + def initialize_options(self) -> None: + self.pkg_name: Optional[str] = None - def run(self): + def finalize_options(self) -> None: + self.pkg_name = self.distribution.get_name().replace("-", "_") + + def get_source_files(self) -> List[str]: + return ["uap-core/regexes.yaml"] + + def get_outputs(self) -> List[str]: + return [f"{self.pkg_name}/_regexes.py"] + + def get_output_mapping(self) -> Dict[str, str]: + return dict(zip(self.get_source_files(), self.get_outputs())) + + def run(self) -> None: + # FIXME: check git / submodules? + """ work_path = self.work_path if not os.path.exists(os.path.join(work_path, ".git")): return @@ -70,47 +41,44 @@ def run(self): log.info("initializing git submodules") check_output(["git", "submodule", "init"], cwd=work_path) check_output(["git", "submodule", "update"], cwd=work_path) + """ + if not self.pkg_name: + return # or error? - yaml_src = os.path.join(work_path, "uap-core", "regexes.yaml") - if not os.path.exists(yaml_src): + yaml_src = Path("uap-core", "regexes.yaml") + if not yaml_src.is_file(): raise RuntimeError( - "Unable to find regexes.yaml, should be at %r" % yaml_src + f"Unable to find regexes.yaml, should be at {yaml_src!r}" ) - def force_bytes(text): - if text is None: - return text - return text.encode("utf8") - def write_params(fields): # strip trailing None values while len(fields) > 1 and fields[-1] is None: fields.pop() for field in fields: - fp.write((" %r,\n" % field).encode("utf-8")) + fp.write((f" {field!r},\n").encode()) + + with yaml_src.open("rb") as f: + regexes = yaml.safe_load(f) - import yaml + if self.editable_mode: + dist_dir = Path("src") + else: + dist_dir = Path(self.get_finalized_command("bdist_wheel").bdist_dir) - log.info("compiling regexes.yaml -> _regexes.py") - with open(yaml_src, "rb") as fp: - regexes = yaml.safe_load(fp) + outdir = dist_dir / self.pkg_name + outdir.mkdir(parents=True, exist_ok=True) - lib_dest = os.path.join(self.build_lib, "ua_parser") - if not os.path.exists(lib_dest): - os.makedirs(lib_dest) + dest = outdir / "_regexes.py" - py_dest = os.path.join(lib_dest, "_regexes.py") - with open(py_dest, "wb") as fp: + with dest.open("wb") as fp: # fmt: off fp.write(b"# -*- coding: utf-8 -*-\n") - fp.write(b"############################################\n") - fp.write(b"# NOTICE: This file is autogenerated from #\n") - fp.write(b"# regexes.yaml. Do not edit by hand, #\n") - fp.write(b"# instead, re-run `setup.py build_regexes` #\n") - fp.write(b"############################################\n") + fp.write(b"########################################################\n") + fp.write(b"# NOTICE: This file is autogenerated from regexes.yaml #\n") + fp.write(b"########################################################\n") fp.write(b"\n") - fp.write(b"from __future__ import absolute_import, unicode_literals\n") fp.write(b"from .user_agent_parser import (\n") fp.write(b" UserAgentParser, DeviceParser, OSParser,\n") fp.write(b")\n") @@ -157,85 +125,9 @@ def write_params(fields): fp.write(b"]\n") # fmt: on - self.update_manifest() - - def update_manifest(self): - sdist = self.distribution.get_command_obj("sdist") - if not sdist.finalized: - return - - sdist.filelist.files.append("src/ua_parser/_regexes.py") - - -class develop(_develop): - def run(self): - self.run_command("build_regexes") - _develop.run(self) - - -class install(_install): - def run(self): - self.run_command("build_regexes") - _install.run(self) - - -class build(_build): - def run(self): - self.run_command("build_regexes") - _build.run(self) - - -class sdist(_sdist): - sub_commands = _sdist.sub_commands + [("build_regexes", None)] - - -cmdclass = { - "sdist": sdist, - "develop": develop, - "build": build, - "install": install, - "build_regexes": build_regexes, -} - setup( - name="ua-parser", - version="0.16.1", - description="Python port of Browserscope's user agent parser", - author="PBS", - author_email="no-reply@pbs.org", - packages=find_packages(where="src"), - package_dir={"": "src"}, - license="Apache 2.0", - zip_safe=False, - url="https://github.com/ua-parser/uap-python", - include_package_data=True, - setup_requires=["pyyaml"], - install_requires=[], - cmdclass=cmdclass, - classifiers=[ - "Development Status :: 4 - Beta", - "Environment :: Web Environment", - "Intended Audience :: Developers", - "Operating System :: OS Independent", - "License :: OSI Approved :: Apache Software License", - "Programming Language :: Python", - "Topic :: Internet :: WWW/HTTP", - "Topic :: Software Development :: Libraries :: Python Modules", - "Programming Language :: Python", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.3", - "Programming Language :: Python :: 3.4", - "Programming Language :: Python :: 3.5", - "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", - "Programming Language :: Python :: Implementation :: CPython", - "Programming Language :: Python :: Implementation :: PyPy", - ], + cmdclass={ + "compile-regexes": CompileRegexes, + } ) diff --git a/src/ua_parser/user_agent_parser.py b/src/ua_parser/user_agent_parser.py index 4989336..275c12b 100644 --- a/src/ua_parser/user_agent_parser.py +++ b/src/ua_parser/user_agent_parser.py @@ -1,28 +1,8 @@ -# Copyright 2009 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the 'License') -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an 'AS IS' BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Python implementation of the UA parser.""" - -from __future__ import absolute_import - import os import re import sys import warnings -__author__ = "Lindsey Simon " - class UserAgentParser(object): def __init__( diff --git a/tests/test_legacy.py b/tests/test_legacy.py index 9742a66..f4170ce 100644 --- a/tests/test_legacy.py +++ b/tests/test_legacy.py @@ -1,26 +1,3 @@ -# Copyright 2008 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the 'License') -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an 'AS IS' BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -"""User Agent Parser Unit Tests. -""" - - -from __future__ import unicode_literals, absolute_import - -__author__ = "slamm@google.com (Stephen Lamm)" - import logging import os import platform diff --git a/tox.ini b/tox.ini index 6100de9..78420b7 100644 --- a/tox.ini +++ b/tox.ini @@ -1,19 +1,15 @@ [tox] -envlist = py27, py36, py37, py38, py39, py310, py311, +envlist = py38, py39, py310, py311, pypy3.8, pypy3.9, - docs, flake8, black + flake8, black skipsdist = True [testenv] -usedevelop = True deps = -rrequirements_dev.txt commands = + pip install . pytest -Werror --doctest-glob="*.rst" {posargs} -[testenv:py27] -# no doctesting in 2.7 because of formatting divergences -commands = pytest -Werror {posargs} - [testenv:flake8] skip_install = True deps = flake8