diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 57ad3e7..8f17a52 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -1,6 +1,3 @@ -# This workflow will install Python dependencies, run tests and lint with a variety of Python versions -# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions - name: Unit test on: @@ -13,112 +10,55 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - # Github action runner update ubuntu to 22.04, which does not have - # python-3.6 in it: https://github.com/actions/setup-python/issues/544#issuecomment-1320295576 - # To fix it: use os: [ubuntu-20.04] instead os: [ubuntu-latest] python-version: [3.9, "3.10", 3.11, 3.12] steps: - uses: actions/checkout@v5 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip - pip install pytest - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - if [ -f test-requirements.txt ]; then pip install -r test-requirements.txt; fi - - name: Install npm dependencies - run: | - if [ -f package.json ]; then npm install; fi - - # manually add module binary path to github ci - echo "add node module path: $GITHUB_WORKSPACE/node_modules/.bin/" - echo "$GITHUB_WORKSPACE/node_modules/.bin/" >> $GITHUB_PATH - - - name: Install apt dependencies - run: | - if [ -f packages.txt ]; then cat packages.txt | xargs sudo apt-get install; fi + pip install pytest k3ut + pip install -e . - name: Test with pytest - env: - # interactive command such as k3handy.cmdtty to run git, git complains - # if no TERM set: - # out: - (press RETURN) - # err: WARNING: terminal is not fully functional - # And waiting for a RETURN to press for ever - TERM: xterm run: | - cp setup.py .. - cd .. - python setup.py install - cd - - - if [ -f sudo_test ]; then - sudo env "PATH=$PATH" pytest -v - else - pytest -v - fi - - - uses: actions/upload-artifact@v4 - if: failure() - with: - path: test/ + pytest -v build_doc: - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest] - python-version: [3.9, "3.10", 3.11, 3.12] + runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + - name: Set up Python + uses: actions/setup-python@v5 with: - python-version: ${{ matrix.python-version }} + python-version: "3.12" - - name: Install dependencies - run: | - python -m pip install --upgrade pip - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - if [ -f test-requirements.txt ]; then pip install -r test-requirements.txt; fi - - - name: Test building doc + - name: Build docs run: | - pip install -r _building/building-requirements.txt - make -C docs html + pip install -e . + pip install mkdocs mkdocs-material "mkdocstrings[python]" + mkdocs build lint: - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest] - python-version: [3.9, "3.10", 3.11, 3.12] + runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + - name: Set up Python + uses: actions/setup-python@v5 with: - python-version: ${{ matrix.python-version }} - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install flake8 - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - if [ -f test-requirements.txt ]; then pip install -r test-requirements.txt; fi + python-version: "3.12" - - name: Lint with flake8 + - name: Lint with ruff run: | - # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + pip install ruff + ruff check . + ruff format --check . diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 3fd68f1..af931b2 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -1,6 +1,3 @@ -# This workflows will upload a Python Package using Twine when a release is created -# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries - name: Upload Python Package on: @@ -10,27 +7,26 @@ on: jobs: deploy: - runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.x' - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install setuptools wheel twine - - name: Build and publish - env: - TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} - TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} - run: | - cp setup.py .. - cd .. - python setup.py sdist bdist_wheel - pip install dist/*.tar.gz - python -c 'import '${GITHUB_REPOSITORY#*/} - twine upload dist/* + - uses: actions/checkout@v5 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build twine + + - name: Build package + run: python -m build + + - name: Publish to PyPI + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + run: twine upload dist/* diff --git a/.gitignore b/.gitignore index 0569721..94d8aab 100644 --- a/.gitignore +++ b/.gitignore @@ -131,3 +131,4 @@ dmypy.json .pyre/ .claude/ +site/ diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..3e517df --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,16 @@ +version: 2 + +build: + os: ubuntu-22.04 + tools: + python: "3.11" + +mkdocs: + configuration: mkdocs.yml + +python: + install: + - method: pip + path: . + extra_requirements: + - docs diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index eeb740e..0000000 --- a/.travis.yml +++ /dev/null @@ -1,41 +0,0 @@ -language: python - -# cache pip dependency -cache: pip - -sudo: required - -python: - - "3.6" - - "3.7" - - "3.8" - - pypy3 -install: - - | - pip install flake8 pytest - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - if [ -f test-requirements.txt ]; then pip install -r test-requirements.txt; fi - if [ -f package.json ]; then npm install; fi - if [ -f packages.txt ]; then cat packages.txt | xargs sudo apt-get install; fi - pip install -r _building/building-requirements.txt -script: - - sudo env "PATH=$PATH" python --version # environment check - - sudo env "PATH=$PATH" python -c 'import sys; print(sys.version)' # environment check - - sudo env "PATH=$PATH" python -c 'import sys; print(sys.version_info)' # environment check - - # Enable debug log: UT_DEBUG=1 - - | - - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - - cp setup.py .. - cd .. - python setup.py install - cd - - sudo env "PATH=$PATH:$(npm bin)" pytest -v - - make -C docs html - -# after_success: -# - pip install python-coveralls && coveralls diff --git a/__init__.py b/__init__.py index f8f344e..d5df17d 100644 --- a/__init__.py +++ b/__init__.py @@ -1,25 +1,15 @@ -""" -Utility functions for network related operation. -""" +from importlib.metadata import version -# from .proc import CalledProcessError -# from .proc import ProcError - -__version__ = "0.1.0" -__name__ = "k3net" +__version__ = version("k3net") from .net import ( - INN, PUB, - LOCALHOST, - NetworkError, IPUnreachable, InvalidIP4, InvalidIP4Number, - choose_by_idc, choose_ips, choose_inn, @@ -39,29 +29,27 @@ ) __all__ = [ - 'INN', - 'PUB', - - 'LOCALHOST', - - 'NetworkError', - 'IPUnreachable', - 'InvalidIP4', - 'InvalidIP4Number', - - 'choose_by_idc', - 'choose_inn', - 'choose_pub', - 'choose_by_regex', - 'get_host_devices', - 'get_host_ip4', - 'ip_class', - 'ips_prefer', - 'is_inn', - 'is_pub', - 'is_ip4', - 'is_ip4_loopback', - 'parse_ip_regex_str', - 'ip_to_num', - 'num_to_ip', + "INN", + "PUB", + "LOCALHOST", + "NetworkError", + "IPUnreachable", + "InvalidIP4", + "InvalidIP4Number", + "choose_by_idc", + "choose_ips", + "choose_inn", + "choose_pub", + "choose_by_regex", + "get_host_devices", + "get_host_ip4", + "ip_class", + "ips_prefer", + "is_inn", + "is_pub", + "is_ip4", + "is_ip4_loopback", + "parse_ip_regex_str", + "ip_to_num", + "num_to_ip", ] diff --git a/_building/.gitignore b/_building/.gitignore deleted file mode 100644 index b6e4761..0000000 --- a/_building/.gitignore +++ /dev/null @@ -1,129 +0,0 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -pip-wheel-metadata/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -.python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ diff --git a/_building/Makefile b/_building/Makefile deleted file mode 100644 index e5b5931..0000000 --- a/_building/Makefile +++ /dev/null @@ -1,3 +0,0 @@ -# make readme name -readme: - cd .. && python _building/build_readme.py diff --git a/_building/README.md b/_building/README.md index 50205e1..0ee458e 100644 --- a/_building/README.md +++ b/_building/README.md @@ -1,23 +1,24 @@ -# building -building toolkit for pykit3 repos +# _building -This repo should be included in a package, e.g.: +Shared build configuration for pykit3 packages. -``` -vcs/pykit3/k3handy/ -▸ .git/ -▸ .github/ -▸ __pycache__/ -▾ _building/ <-- this repo - ... -``` +## Commands -# Publish python package: +All commands use the `pk3` package: -- `make build_setup_py` does the following steps: - - Builds the `setup.py` and commit it. - - Add a git tag with the name of `"v" + __init__.__ver__`. +```bash +make test # Run tests with pytest +make lint # Format and lint with ruff +make cov # Generate coverage report +make doc # Build documentation with mkdocs +make readme # Generate README.md from docstrings +make release # Create git tag from version in pyproject.toml +make publish # Build and upload to PyPI +``` -- Then `git push` the tag, github Action in the `.github/workflows/python-pubish.yml` will publish a package to `pypi`. +## Release Process - The action spec is copied from template repo: `github.com/pykit3/tmpl`. +1. Update version in `pyproject.toml` +2. Run `make release` to create git tag +3. Run `git push --tags` to trigger GitHub Actions +4. GitHub Actions automatically publishes to PyPI diff --git a/_building/README.md.j2 b/_building/README.md.j2 deleted file mode 100644 index d9baf2c..0000000 --- a/_building/README.md.j2 +++ /dev/null @@ -1,38 +0,0 @@ -# {{ name }} - -[![Action-CI](https://github.com/pykit3/{{ name }}/actions/workflows/python-package.yml/badge.svg)](https://github.com/pykit3/{{ name }}/actions/workflows/python-package.yml) -[![Build Status](https://travis-ci.com/pykit3/{{ name }}.svg?branch=master)](https://travis-ci.com/pykit3/{{ name }}) -[![Documentation Status](https://readthedocs.org/projects/{{ name }}/badge/?version=stable)](https://{{ name }}.readthedocs.io/en/stable/?badge=stable) -[![Package](https://img.shields.io/pypi/pyversions/{{ name }})](https://pypi.org/project/{{ name }}) - -{{ description }} - -{{ name }} is a component of [pykit3] project: a python3 toolkit set. - -{{ package_doc }} - - -# Install - -``` -pip install {{ name }} -``` - -# Synopsis - -```python -{{ synopsis }} -``` - -# Author - -Zhang Yanpo (张炎泼) - -# Copyright and License - -The MIT License (MIT) - -Copyright (c) 2015 Zhang Yanpo (张炎泼) - - -[pykit3]: https://github.com/pykit3 diff --git a/_building/__init__.py b/_building/__init__.py deleted file mode 100644 index 1055c67..0000000 --- a/_building/__init__.py +++ /dev/null @@ -1,129 +0,0 @@ -#!/usr/bin/env python -# coding: utf-8 - -import importlib.util -import sys - -# sys.path.insert(0, os.path.abspath('..')) -# sys.path.insert(0, os.path.abspath('../..')) -# sys.path.insert(0, os.path.abspath('../../..')) - -# __title__ = 'requests' -# __description__ = 'Python HTTP for Humans.' -# __url__ = 'https://requests.readthedocs.io' -# __version__ = '2.23.0' - -__author__ = "Zhang Yanpo" -__author_email__ = "drdr.xp@gmail.com" -__license__ = "MIT" -__copyright__ = "Copyright 2020 Zhang Yanpo" - - -# Configuration file for the Sphinx documentation builder. -# -# This file only contains a selection of the most common options. For a full -# list see the documentation: -# https://www.sphinx-doc.org/en/master/usage/configuration.html - - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = [ - "sphinx.ext.autodoc", - "sphinx.ext.napoleon", - # "sphinx.ext.intersphinx", - # "sphinx.ext.todo", - # "sphinx.ext.viewcode", -] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ["_templates"] - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This pattern also affects html_static_path and html_extra_path. -exclude_patterns = [] - - -master_doc = "index" - - -# -- Options for HTML output ------------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# -html_theme = "alabaster" - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -# html_static_path = ['_static'] -html_static_path = [] - - -def load_parent_package(): - """ - Load the parent directory as a package module. - - Returns: - tuple: (package_name, package_module) - """ - import os - - parent_dir = os.path.dirname(os.path.dirname(__file__)) - if parent_dir not in sys.path: - sys.path.insert(0, parent_dir) - - # Read the __init__.py file to get the package name - init_file = os.path.join(parent_dir, "__init__.py") - package_name = None - - with open(init_file, "r") as f: - for line in f: - if line.strip().startswith("__name__"): - # Extract package name from __name__ = "package_name" - package_name = line.split("=")[1].strip().strip("\"'") - break - - if not package_name: - # Fallback: use directory name - package_name = os.path.basename(parent_dir) - - # Load the module with proper package context using importlib - spec = importlib.util.spec_from_file_location(package_name, init_file) - pkg = importlib.util.module_from_spec(spec) - sys.modules[package_name] = pkg # Add to sys.modules so relative imports work - spec.loader.exec_module(pkg) - - return package_name, pkg - - -def sphinx_confs(): - """ - Load repo dir as a package - - `readthedocs` use branch name as dir! - Thus the following does not work:: - - import pk3proc - """ - - print("sys.path:", sys.path) - - package_name, pkg = load_parent_package() - - return ( - pkg.__name__, - pkg, - pkg.__version__, - __author__, - __copyright__, - extensions, - templates_path, - exclude_patterns, - master_doc, - html_theme, - html_static_path, - ) diff --git a/_building/build_readme.py b/_building/build_readme.py deleted file mode 100644 index d211e29..0000000 --- a/_building/build_readme.py +++ /dev/null @@ -1,85 +0,0 @@ -#!/usr/bin/env python -# coding: utf-8 - -import doctest -import os -import sys - -import jinja2 -import yaml - -from __init__ import load_parent_package - -# xxx/_building/build_readme.py -this_base = os.path.dirname(__file__) - -j2vars = {} - -# let it be able to find indirectly dependent package locally -# e.g.: `k3fs` depends on `k3confloader` -sys.path.insert(0, os.path.abspath("..")) - -# load package name from __init__.py -package_name, pkg = load_parent_package() -j2vars["name"] = package_name - - -def get_gh_config(): - with open(".github/settings.yml", "r") as f: - cont = f.read() - - cfg = yaml.safe_load(cont) - tags = cfg["repository"]["topics"].split(",") - tags = [x.strip() for x in tags] - cfg["repository"]["topics"] = tags - return cfg - - -cfg = get_gh_config() -j2vars["description"] = cfg["repository"]["description"] - - -def get_examples(pkg): - doc = pkg.__doc__ - parser = doctest.DocTestParser() - es = parser.get_examples(doc) - rst = [] - for e in es: - rst.append(">>> " + e.source.strip()) - rst.append(e.want.strip()) - - rst = "\n".join(rst) - - for fn in ( - "synopsis.txt", - "synopsis.py", - ): - try: - with open(fn, "r") as f: - rst += "\n" + f.read() - - except FileNotFoundError: - pass - - return rst - - -j2vars["synopsis"] = get_examples(pkg) -j2vars["package_doc"] = pkg.__doc__ - - -def render_j2(tmpl_path, tmpl_vars, output_path): - template_loader = jinja2.FileSystemLoader(searchpath="./") - template_env = jinja2.Environment( - loader=template_loader, undefined=jinja2.StrictUndefined - ) - template = template_env.get_template(tmpl_path) - - txt = template.render(tmpl_vars) - - with open(output_path, "w") as f: - f.write(txt) - - -if __name__ == "__main__": - render_j2("_building/README.md.j2", j2vars, "README.md") diff --git a/_building/build_setup.py b/_building/build_setup.py deleted file mode 100644 index ca2ec6c..0000000 --- a/_building/build_setup.py +++ /dev/null @@ -1,190 +0,0 @@ -#!/usr/bin/env python -# coding: utf-8 - -""" -build steup.py for this package. -""" - -import ast -import subprocess -import sys -from string import Template - -import requirements -import yaml - -if hasattr(sys, "getfilesystemencoding"): - defenc = sys.getfilesystemencoding() -if defenc is None: - defenc = sys.getdefaultencoding() - - -def parse_assignment(filename, var_name): - """Parse a Python file and extract the value of a variable assignment using AST.""" - with open(filename, "r") as f: - content = f.read() - - tree = ast.parse(content) - - for node in ast.walk(tree): - if isinstance(node, ast.Assign): - # Check if this assignment is to our target variable - for target in node.targets: - if isinstance(target, ast.Name) and target.id == var_name: - # Extract the literal value - if isinstance(node.value, ast.Constant): # Python 3.8+ - return node.value.value - elif isinstance(node.value, ast.Str): # Python < 3.8 - return node.value.s - elif isinstance(node.value, ast.Num): # Python < 3.8 - return node.value.n - - return None - - -def get_name(): - name = parse_assignment("__init__.py", "__name__") - return name if name is not None else "k3git" # fallback - - -name = get_name() - - -def get_ver(): - version = parse_assignment("__init__.py", "__version__") - if version is None: - raise ValueError("Could not find __version__ in __init__.py") - return version - - -def get_gh_config(): - with open(".github/settings.yml", "r") as f: - cont = f.read() - - cfg = yaml.safe_load(cont) - tags = cfg["repository"]["topics"].split(",") - tags = [x.strip() for x in tags] - cfg["repository"]["topics"] = tags - return cfg - - -def get_travis(): - try: - with open(".travis.yml", "r") as f: - cont = f.read() - except OSError: - return None - - cfg = yaml.safe_load(cont) - return cfg - - -def get_compatible(): - # https://pypi.org/classifiers/ - - rst = [] - t = get_travis() - if t is None: - return ["Programming Language :: Python :: 3"] - - for v in t["python"]: - if v.startswith("pypy"): - v = "Implementation :: PyPy" - rst.append("Programming Language :: Python :: {}".format(v)) - - return rst - - -def get_req(): - try: - with open("requirements.txt", "r") as f: - req = list(requirements.parse(f)) - except OSError: - req = [] - - # req.name, req.specs, req.extras - # Django [('>=', '1.11'), ('<', '1.12')] - # six [('==', '1.10.0')] - req = [x.name + ",".join([a + b for a, b in x.specs]) for x in req] - - return req - - -cfg = get_gh_config() - -ver = get_ver() -description = cfg["repository"]["description"] -long_description = open("README.md").read() -req = get_req() -prog = get_compatible() - - -tmpl = """# DO NOT EDIT!!! built with `python _building/build_setup.py` -import setuptools -setuptools.setup( - name="${name}", - packages=["${name}"], - version="$ver", - license='MIT', - description=$description, - long_description=$long_description, - long_description_content_type="text/markdown", - author='Zhang Yanpo', - author_email='drdr.xp@gmail.com', - url='https://github.com/pykit3/$name', - keywords=$topics, - python_requires='>=3.0', - - install_requires=$req, - classifiers=[ - 'Development Status :: 4 - Beta', - 'Intended Audience :: Developers', - 'Topic :: Software Development :: Libraries', - ] + $prog, -) -""" - -s = Template(tmpl) -rst = s.substitute( - name=name, - ver=ver, - description=repr(description), - long_description=repr(long_description), - topics=repr(cfg["repository"]["topics"]), - req=repr(req), - prog=repr(prog), -) -with open("setup.py", "w") as f: - f.write(rst) - - -sb = subprocess.Popen( - ["git", "add", "setup.py"], - encoding=defenc, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, -) -out, err = sb.communicate() -if sb.returncode != 0: - raise Exception("failure to add: ", out, err) - -sb = subprocess.Popen( - ["git", "commit", "setup.py", "-m", "release: v" + ver], - encoding=defenc, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, -) -out, err = sb.communicate() -if sb.returncode != 0: - raise Exception("failure to commit new release: " + ver, out, err) - - -sb = subprocess.Popen( - ["git", "tag", "v" + ver], - encoding=defenc, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, -) -out, err = sb.communicate() -if sb.returncode != 0: - raise Exception("failure to add tag: " + ver, out, err) diff --git a/_building/building-requirements.txt b/_building/building-requirements.txt deleted file mode 100644 index bd303df..0000000 --- a/_building/building-requirements.txt +++ /dev/null @@ -1,7 +0,0 @@ -# requirements for building doc, setup.py etc - -semantic_version>=2.9,<3.0 -jinja2>=3.0,<4.0 -PyYAML>=6.0,<7.0 - -sphinx>=4.3,<6.0 diff --git a/_building/common.mk b/_building/common.mk index dbac102..571715a 100644 --- a/_building/common.mk +++ b/_building/common.mk @@ -1,14 +1,19 @@ all: test lint readme doc -.PHONY: test lint +.PHONY: test lint cov sudo_test: - sudo env "PATH=$$PATH" UT_DEBUG=0 PYTHONPATH="$$(cd ..; pwd)" python -m unittest discover -c --failfast -s . + sudo env "PATH=$$PATH" UT_DEBUG=0 pytest -v test: - env "PATH=$$PATH" UT_DEBUG=0 PYTHONPATH="$$(cd ..; pwd)" python -m unittest discover -c --failfast -s . + env UT_DEBUG=0 pytest -v + +cov: + coverage run --source=. -m pytest + coverage html + open htmlcov/index.html doc: - make -C docs html + mkdocs build lint: # ruff format: fast Python code formatter (Black-compatible) @@ -21,13 +26,13 @@ static_check: uvx mypy . --ignore-missing-imports readme: - python _building/build_readme.py + pk3 readme -build_setup_py: - PYTHONPATH="$$(cd ..; pwd)" python _building/build_setup.py +release: + pk3 tag publish: - ./_building/publish.sh + pk3 publish install: - ./_building/install.sh + pip install -e . diff --git a/_building/install.sh b/_building/install.sh deleted file mode 100755 index e82727c..0000000 --- a/_building/install.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/sh - - -pwd="$(pwd)" -name="${pwd##*/}" -pip uninstall -y $name - -cp setup.py .. -( -cd .. -python setup.py install -) diff --git a/_building/populate.py b/_building/populate.py index 77deb12..d0a8731 100755 --- a/_building/populate.py +++ b/_building/populate.py @@ -15,22 +15,22 @@ def pjoin(*args): def cp(fn): - - cur = os.path.abspath('.') + cur = os.path.abspath(".") name = os.path.split(cur)[1] - t = '_building/tmpl/' - relfn = fn[len(t):] + t = "_building/tmpl/" + relfn = fn[len(t) :] base = os.path.split(fn)[0] if not os.path.exists(base): os.makedirs(base) src = fn - dst = re.sub(r'xxnamexx', name, relfn) + dst = re.sub(r"xxnamexx", name, relfn) - vs = {'name': name, - 'nameBig': name[0].upper() + name[1:], - } + vs = { + "name": name, + "nameBig": name[0].upper() + name[1:], + } print("populate ", src, " to ", dst) render_j2(src, vs, dst) @@ -39,14 +39,13 @@ def cp(fn): def render_j2(tmpl_path, tmpl_vars, output_path): - template_loader = jinja2.FileSystemLoader(searchpath='./') - template_env = jinja2.Environment(loader=template_loader, - undefined=jinja2.StrictUndefined) + template_loader = jinja2.FileSystemLoader(searchpath="./") + template_env = jinja2.Environment(loader=template_loader, undefined=jinja2.StrictUndefined) template = template_env.get_template(tmpl_path) txt = template.render(tmpl_vars) - with open(output_path, 'w') as f: + with open(output_path, "w") as f: f.write(txt) diff --git a/_building/requirements.txt b/_building/requirements.txt deleted file mode 100644 index 49fe098..0000000 --- a/_building/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -setuptools diff --git a/docs/Makefile b/docs/Makefile deleted file mode 100644 index a4de0bf..0000000 --- a/docs/Makefile +++ /dev/null @@ -1,20 +0,0 @@ -# Minimal makefile for Sphinx documentation -# - -# You can set these variables from the command line, and also -# from the environment for the first two. -SPHINXOPTS ?= -W -SPHINXBUILD ?= sphinx-build -SOURCEDIR = source -BUILDDIR = build - -# Put it first so that "make" without argument is like "make help". -help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - -.PHONY: help Makefile - -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..8e69e4e --- /dev/null +++ b/docs/index.md @@ -0,0 +1,45 @@ +# k3net + +[![Action-CI](https://github.com/pykit3/k3net/actions/workflows/python-package.yml/badge.svg)](https://github.com/pykit3/k3net/actions/workflows/python-package.yml) +[![Documentation Status](https://readthedocs.org/projects/k3net/badge/?version=stable)](https://k3net.readthedocs.io/en/stable/?badge=stable) +[![Package](https://img.shields.io/pypi/pyversions/k3net)](https://pypi.org/project/k3net) + +Network utilities for IP address detection and classification. Get host IP addresses, classify public/private IPs. + +k3net is a component of [pykit3](https://github.com/pykit3) project: a python3 toolkit set. + +## Installation + +```bash +pip install k3net +``` + +## Quick Start + +```python +import k3net + +# Get host IP addresses +ips = k3net.get_host_ip4() +print(ips) # ['192.168.1.100', '10.0.0.1'] + +# Check if IP is public or private +print(k3net.is_pub('8.8.8.8')) # True +print(k3net.is_inn('10.0.0.1')) # True + +# Classify IP +print(k3net.ip_class('192.168.1.1')) # 'INN' +print(k3net.ip_class('8.8.8.8')) # 'PUB' + +# Convert IP to number and back +num = k3net.ip_to_num('192.168.1.1') +ip = k3net.num_to_ip(num) +``` + +## API Reference + +::: k3net + +## License + +The MIT License (MIT) - Copyright (c) 2015 Zhang Yanpo (张炎泼) diff --git a/docs/make.bat b/docs/make.bat deleted file mode 100644 index 6247f7e..0000000 --- a/docs/make.bat +++ /dev/null @@ -1,35 +0,0 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set SOURCEDIR=source -set BUILDDIR=build - -if "%1" == "" goto help - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% - -:end -popd diff --git a/docs/source/conf.py b/docs/source/conf.py deleted file mode 100644 index e436b3e..0000000 --- a/docs/source/conf.py +++ /dev/null @@ -1,28 +0,0 @@ -import os -import sys - -sys.path.insert(0, os.path.abspath("../..")) - -# In order to find indirect dependency -sys.path.insert(0, os.path.abspath("../../..")) - -# use a try to force not to reorder sys.path and import. -try: - import _building -except Exception as e: - raise e - - -( - project, - pkg, - release, - author, - copyright, - extensions, - templates_path, - exclude_patterns, - master_doc, - html_theme, - html_static_path, -) = _building.sphinx_confs() diff --git a/docs/source/index.rst b/docs/source/index.rst deleted file mode 100644 index b8c8399..0000000 --- a/docs/source/index.rst +++ /dev/null @@ -1,66 +0,0 @@ -.. k3net documentation master file, created by - sphinx-quickstart on Thu May 14 16:58:55 2020. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - -k3net -============ - -.. automodule:: k3net - -Documentation for the Code -************************** - -Exceptions ----------- - -.. autoexception:: NetworkError - -.. autoexception:: IPUnreachable - -.. autoexception:: InvalidIP4 - -.. autoexception:: InvalidIP4Number - - -Functions ---------- - -.. autofunction:: choose_by_idc - -.. autofunction:: choose_ips - -.. autofunction:: choose_inn - -.. autofunction:: choose_pub - -.. autofunction:: choose_by_regex - -.. autofunction:: get_host_devices - -.. autofunction:: get_host_ip4 - -.. autofunction:: ip_class - -.. autofunction:: ips_prefer - -.. autofunction:: is_inn - -.. autofunction:: is_pub - -.. autofunction:: is_ip4 - -.. autofunction:: is_ip4_loopback - -.. autofunction:: parse_ip_regex_str - -.. autofunction:: ip_to_num - -.. autofunction:: num_to_ip - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..77e038a --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,30 @@ +site_name: k3net +site_description: DESCRIPTION +site_url: https://k3net.readthedocs.io +repo_url: https://github.com/pykit3/k3net +repo_name: pykit3/k3net + +theme: + name: material + palette: + primary: blue + accent: blue + +plugins: + - search + - mkdocstrings: + handlers: + python: + paths: [.] + options: + show_source: true + show_root_heading: true + heading_level: 2 + +nav: + - Home: index.md + +markdown_extensions: + - admonition + - pymdownx.highlight + - pymdownx.superfences diff --git a/net.py b/net.py index f993fdc..7120bef 100644 --- a/net.py +++ b/net.py @@ -10,15 +10,15 @@ """ It is string "PUB" that represents a public ip. """ -PUB = 'PUB' +PUB = "PUB" """ It is string "INN" that represents a internal ip. """ -INN = 'INN' +INN = "INN" -LOCALHOST = '127.0.0.1' -inner_ip_patterns = ['^172[.]1[6-9].*', '^172[.]2[0-9].*', '^172[.]3[0-1].*', '^10[.].*', '^192[.]168[.].*'] +LOCALHOST = "127.0.0.1" +inner_ip_patterns = ["^172[.]1[6-9].*", "^172[.]2[0-9].*", "^172[.]3[0-1].*", "^10[.].*", "^192[.]168[.].*"] logger = logging.getLogger(__name__) @@ -26,6 +26,7 @@ class NetworkError(Exception): """ Super class of all network exceptions. """ + pass @@ -33,6 +34,7 @@ class IPUnreachable(NetworkError): """ Exception for an unreachable ip. """ + pass @@ -40,6 +42,7 @@ class InvalidIP4(Exception): """ Exception for an invalidIP4 ip. """ + pass @@ -47,6 +50,7 @@ class InvalidIP4Number(Exception): """ Exception for an invalidIP4 number. """ + pass @@ -59,10 +63,9 @@ def is_ip4(ip): if not isinstance(ip, (str, bytes)): return False - ip = ip.split('.') + ip = ip.split(".") for s in ip: - if not s.isdigit(): return False @@ -79,11 +82,10 @@ def ip_class(ip): :param ip: :return: `net.PUB` or `net.INN`. """ - if ip.startswith('127.0.0.'): + if ip.startswith("127.0.0."): return INN for ptn in inner_ip_patterns: - if re.match(ptn, ip): return INN @@ -92,7 +94,7 @@ def ip_class(ip): def is_ip4_loopback(ip): - return is_ip4(ip) and ip.startswith('127.') + return is_ip4(ip) and ip.startswith("127.") def ips_prefer(ips, preference): @@ -154,7 +156,7 @@ def choose_ips(ips, ip_type=None): elif ip_type == PUB: return choose_pub(ips) else: - raise ValueError('invalid ip_type: {ip_type}'.format(ip_type=ip_type)) + raise ValueError("invalid ip_type: {ip_type}".format(ip_type=ip_type)) def choose_pub(ips): @@ -191,7 +193,7 @@ def get_host_ip4(iface_prefix=None, exclude_prefix=None): :return: a list of ipv4 addresses. """ if iface_prefix is None: - iface_prefix = [''] + iface_prefix = [""] if isinstance(iface_prefix, (str, bytes)): iface_prefix = [iface_prefix] @@ -203,7 +205,6 @@ def get_host_ip4(iface_prefix=None, exclude_prefix=None): ips = [] for ifacename in netifaces.interfaces(): - matched = False for t in iface_prefix: @@ -223,10 +224,8 @@ def get_host_ip4(iface_prefix=None, exclude_prefix=None): addrs = netifaces.ifaddresses(ifacename) if netifaces.AF_INET in addrs and netifaces.AF_LINK in addrs: - for addr in addrs[netifaces.AF_INET]: - - ip = str(addr['addr']) + ip = str(addr["addr"]) if not is_ip4_loopback(ip): ips.append(ip) @@ -250,47 +249,44 @@ def choose_by_idc(dest_idc, local_idc, ips): return pref_ips -def get_host_devices(iface_prefix=''): +def get_host_devices(iface_prefix=""): """ - Returns a dictionary of all iface, and address information those are binded to it. + Returns a dictionary of all iface, and address information those are binded to it. -{ - 'en0': { - 'LINK': [{ - 'addr': 'ac:bc:32:8f:e5:71' - }], - 'INET': [{ - 'broadcast': '172.18.5.255', - 'netmask': '255.255.255.0', - 'addr': '172.18.5.252' + { + 'en0': { + 'LINK': [{ + 'addr': 'ac:bc:32:8f:e5:71' + }], + 'INET': [{ + 'broadcast': '172.18.5.255', + 'netmask': '255.255.255.0', + 'addr': '172.18.5.252' - }] + }] - } + } -} - :param iface_prefix: is a string or `''` to specify what iface should be chosen. - :return: a dictionary of iface and its address information. + } + :param iface_prefix: is a string or `''` to specify what iface should be chosen. + :return: a dictionary of iface and its address information. """ rst = {} for ifacename in netifaces.interfaces(): - if not ifacename.startswith(iface_prefix): continue addrs = netifaces.ifaddresses(ifacename) if netifaces.AF_INET in addrs and netifaces.AF_LINK in addrs: - - ips = [addr['addr'] for addr in addrs[netifaces.AF_INET]] + ips = [addr["addr"] for addr in addrs[netifaces.AF_INET]] for ip in ips: if is_ip4_loopback(ip): break else: - rst[ifacename] = {'INET': addrs[netifaces.AF_INET], - 'LINK': addrs[netifaces.AF_LINK]} + rst[ifacename] = {"INET": addrs[netifaces.AF_INET], "LINK": addrs[netifaces.AF_LINK]} return rst @@ -309,17 +305,17 @@ def parse_ip_regex_str(ip_regexs_str): """ ip_regexs_str = ip_regexs_str.strip() - regs = ip_regexs_str.split(',') + regs = ip_regexs_str.split(",") rst = [] for r in regs: # do not choose ip if it matches this regex - if r.startswith('-'): + if r.startswith("-"): r = (r[1:], False) else: r = (r, True) - if r[0] == '': - raise ValueError('invalid regular expression: ' + repr(r)) + if r[0] == "": + raise ValueError("invalid regular expression: " + repr(r)) if r[1]: r = r[0] @@ -339,10 +335,8 @@ def choose_by_regex(ips, ip_regexs): rst = [] for ip in ips: - all_negative = True for ip_regex in ip_regexs: - # choose matched: # '127[.]' # ('127[.]', True) @@ -380,9 +374,9 @@ def ip_to_num(ip_str): :return: a 4-byte integer. """ if not is_ip4(ip_str): - raise InvalidIP4('IP is invalid: {s}'.format(s=ip_str)) + raise InvalidIP4("IP is invalid: {s}".format(s=ip_str)) - return struct.unpack('>L', socket.inet_aton(ip_str))[0] + return struct.unpack(">L", socket.inet_aton(ip_str))[0] def num_to_ip(ip_num): @@ -392,22 +386,21 @@ def num_to_ip(ip_num): :return: IP. """ if isinstance(ip_num, bool) or not isinstance(ip_num, int): - raise InvalidIP4Number('The type of IP4 number should be int or long :{t}'.format(t=type(ip_num))) - if ip_num > 0xffffffff or ip_num < 0: - raise InvalidIP4Number('IP4 number should be between 0 and 0xffffffff :{s}'.format(s=ip_num)) + raise InvalidIP4Number("The type of IP4 number should be int or long :{t}".format(t=type(ip_num))) + if ip_num > 0xFFFFFFFF or ip_num < 0: + raise InvalidIP4Number("IP4 number should be between 0 and 0xffffffff :{s}".format(s=ip_num)) - return socket.inet_ntoa(struct.pack('>L', ip_num)) + return socket.inet_ntoa(struct.pack(">L", ip_num)) if __name__ == "__main__": - args = sys.argv[1:] - if args[0] == 'ip': + if args[0] == "ip": print(yaml.dump(get_host_ip4(), default_flow_style=False)) - elif args[0] == 'device': + elif args[0] == "device": print(yaml.dump(get_host_devices(), default_flow_style=False)) else: - raise ValueError('invalid command line arguments', args) + raise ValueError("invalid command line arguments", args) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..c967e45 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,60 @@ +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "k3net" +version = "0.1.1" +description = "Network utilities for IP address detection and classification" +readme = "README.md" +license = {text = "MIT"} +requires-python = ">=3.9" +authors = [ + { name = "Zhang Yanpo", email = "drdr.xp@gmail.com" } +] +keywords = ["network", "ip", "ipv4", "host", "address"] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Topic :: Software Development :: Libraries", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] +dependencies = [ + "netifaces", + "pyyaml", +] + +[project.urls] +Homepage = "https://github.com/pykit3/k3net" +Documentation = "https://k3net.readthedocs.io" + +[project.optional-dependencies] +dev = [ + "pytest>=7.0", + "ruff", + "coverage", +] +publish = [ + "build", + "twine", + "pk3", +] +docs = [ + "mkdocs>=1.5", + "mkdocs-material>=9.0", + "mkdocstrings[python]>=0.24", +] + +[tool.setuptools] +packages = ["k3net"] + +[tool.setuptools.package-dir] +k3net = "." + +[tool.ruff] +line-length = 120 diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 6802175..0000000 --- a/requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ --r _building/requirements.txt - - -k3ut>=0.1.15,<0.2 -netifaces~=0.11.0 -PyYAML>=5.0.0 \ No newline at end of file diff --git a/setup.py b/setup.py deleted file mode 100644 index b485359..0000000 --- a/setup.py +++ /dev/null @@ -1,23 +0,0 @@ -# DO NOT EDIT!!! built with `python _building/build_setup.py` -import setuptools -setuptools.setup( - name="k3net", - packages=["k3net"], - version="0.1.0", - license='MIT', - description='Utility functions for network related operation.', - long_description='# k3net\n\n[![Action-CI](https://github.com/pykit3/k3net/actions/workflows/python-package.yml/badge.svg)](https://github.com/pykit3/k3net/actions/workflows/python-package.yml)\n[![Build Status](https://travis-ci.com/pykit3/k3net.svg?branch=master)](https://travis-ci.com/pykit3/k3net)\n[![Documentation Status](https://readthedocs.org/projects/k3net/badge/?version=stable)](https://k3net.readthedocs.io/en/stable/?badge=stable)\n[![Package](https://img.shields.io/pypi/pyversions/k3net)](https://pypi.org/project/k3net)\n\nUtility functions for network related operation.\n\nk3net is a component of [pykit3] project: a python3 toolkit set.\n\n\nUtility functions for network related operation.\n\n\n\n# Install\n\n```\npip install k3net\n```\n\n# Author\n\nZhang Yanpo (张炎泼) \n\n# Copyright and License\n\nThe MIT License (MIT)\n\nCopyright (c) 2015 Zhang Yanpo (张炎泼) \n\n\n[pykit3]: https://github.com/pykit3', - long_description_content_type="text/markdown", - author='Zhang Yanpo', - author_email='drdr.xp@gmail.com', - url='https://github.com/pykit3/k3net', - keywords=['python', 'net'], - python_requires='>=3.0', - - install_requires=['k3ut<0.2,>=0.1.15', 'netifaces~=0.11.0', 'PyYAML>=5.0.0'], - classifiers=[ - 'Development Status :: 4 - Beta', - 'Intended Audience :: Developers', - 'Topic :: Software Development :: Libraries', - ] + ['Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: Implementation :: PyPy'], -) diff --git a/test/test_k3net.py b/test/test_k3net.py index 2058ddf..853f742 100644 --- a/test/test_k3net.py +++ b/test/test_k3net.py @@ -8,130 +8,113 @@ class TestNet(unittest.TestCase): - def test_const(self): - self.assertEqual('PUB', k3net.PUB) - self.assertEqual('INN', k3net.INN) + self.assertEqual("PUB", k3net.PUB) + self.assertEqual("INN", k3net.INN) - self.assertEqual('127.0.0.1', k3net.LOCALHOST) + self.assertEqual("127.0.0.1", k3net.LOCALHOST) def test_exception(self): [k3net.NetworkError, k3net.IPUnreachable] def test_is_ip4_false(self): - cases_not_ip4 = ( None, True, False, 1, 0, - '', - '1', + "", + "1", (), [], {}, - '1.', - '1.1', - '1.1.', - '1.1.1', - '1.1.1.', - - '.1.1.1', - - 'x.1.1.1', - '1.x.1.1', - '1.1.x.1', - '1.1.1.x', - - '1.1.1.1.', - '.1.1.1.1', - '1:1.1.1', - '1:1:1.1', - - '256.1.1.1', - '1.256.1.1', - '1.1.256.1', - '1.1.1.256', - - '1.1.1.1.', - '1.1.1.1.1', - '1.1.1.1.1.', - '1.1.1.1.1.1', + "1.", + "1.1", + "1.1.", + "1.1.1", + "1.1.1.", + ".1.1.1", + "x.1.1.1", + "1.x.1.1", + "1.1.x.1", + "1.1.1.x", + "1.1.1.1.", + ".1.1.1.1", + "1:1.1.1", + "1:1:1.1", + "256.1.1.1", + "1.256.1.1", + "1.1.256.1", + "1.1.1.256", + "1.1.1.1.", + "1.1.1.1.1", + "1.1.1.1.1.", + "1.1.1.1.1.1", ) for inp in cases_not_ip4: self.assertEqual(False, k3net.is_ip4(inp), inp) def test_is_ip4_true(self): - cases_ip4 = ( - '0.0.0.0', - '0.0.0.1', - '0.0.1.0', - '0.1.0.0', - '1.0.0.0', - - '127.0.0.1', - - '255.255.255.255', + "0.0.0.0", + "0.0.0.1", + "0.0.1.0", + "0.1.0.0", + "1.0.0.0", + "127.0.0.1", + "255.255.255.255", ) for inp in cases_ip4: self.assertEqual(True, k3net.is_ip4(inp), inp) def test_is_ip4_loopback_false(self): - cases_ip4 = ( - '0.0.0.0', - '1.1.1.1', - '126.0.1.0', - '15.1.0.0', - '255.0.0.255', - - '126.0.0.1', - '128.0.0.1', - - '255.255.255.255', + "0.0.0.0", + "1.1.1.1", + "126.0.1.0", + "15.1.0.0", + "255.0.0.255", + "126.0.0.1", + "128.0.0.1", + "255.255.255.255", ) for ip in cases_ip4: self.assertEqual(False, k3net.is_ip4_loopback(ip), ip) def test_is_ip4_loopback_true(self): - cases_ip4 = ( - '127.0.0.0', - '127.1.1.1', - '127.0.1.0', - '127.1.0.0', - '127.0.0.255', - - '127.0.0.1', - - '127.255.255.255', + "127.0.0.0", + "127.1.1.1", + "127.0.1.0", + "127.1.0.0", + "127.0.0.255", + "127.0.0.1", + "127.255.255.255", ) for ip in cases_ip4: self.assertEqual(True, k3net.is_ip4_loopback(ip), ip) def test_ip_class_and_is_xxx(self): - - self.assertRaises(ValueError, k3net.choose_ips, ['192.168.0.0'], 'xx') + self.assertRaises(ValueError, k3net.choose_ips, ["192.168.0.0"], "xx") cases_pub = ( - '1.2.3.4', - '255.255.0.0', - '171.0.0.0', - '173.0.0.0', - '172.15.0.0', - '172.32.0.0', - '9.0.0.0', - '11.0.0.0', - '192.167.0.0', - '192.169.0.0', - '191.168.0.0', - '193.168.0.0', + "1.2.3.4", + "255.255.0.0", + "171.0.0.0", + "173.0.0.0", + "172.15.0.0", + "172.32.0.0", + "9.0.0.0", + "11.0.0.0", + "192.167.0.0", + "192.169.0.0", + "191.168.0.0", + "193.168.0.0", ) for inp in cases_pub: @@ -142,24 +125,24 @@ def test_ip_class_and_is_xxx(self): self.assertEqual(False, k3net.is_inn(inp)) # test choose_xxx - self.assertEqual([inp], k3net.choose_pub([inp, '192.168.0.0'])) - self.assertEqual([inp], k3net.choose_pub(['192.168.0.0', inp])) + self.assertEqual([inp], k3net.choose_pub([inp, "192.168.0.0"])) + self.assertEqual([inp], k3net.choose_pub(["192.168.0.0", inp])) - self.assertEqual([inp], k3net.choose_ips([inp, '192.168.0.0'], k3net.PUB)) - self.assertEqual([inp], k3net.choose_ips(['192.168.0.0', inp], k3net.PUB)) - self.assertEqual([inp, '192.168.0.0'], k3net.choose_ips([inp, '192.168.0.0'])) - self.assertEqual(['192.168.0.0', inp], k3net.choose_ips(['192.168.0.0', inp])) + self.assertEqual([inp], k3net.choose_ips([inp, "192.168.0.0"], k3net.PUB)) + self.assertEqual([inp], k3net.choose_ips(["192.168.0.0", inp], k3net.PUB)) + self.assertEqual([inp, "192.168.0.0"], k3net.choose_ips([inp, "192.168.0.0"])) + self.assertEqual(["192.168.0.0", inp], k3net.choose_ips(["192.168.0.0", inp])) cases_inn = ( - '127.0.0.1', - '127.0.0.255', - '172.16.0.0', - '172.17.0.0', - '172.21.0.0', - '172.30.0.0', - '172.31.0.0', - '10.0.0.0', - '192.168.0.0', + "127.0.0.1", + "127.0.0.255", + "172.16.0.0", + "172.17.0.0", + "172.21.0.0", + "172.30.0.0", + "172.31.0.0", + "10.0.0.0", + "192.168.0.0", ) for inp in cases_inn: @@ -171,31 +154,26 @@ def test_ip_class_and_is_xxx(self): self.assertEqual(False, k3net.is_pub(inp)) # test choose_xxx - self.assertEqual([inp], k3net.choose_inn([inp, '1.1.1.1'])) - self.assertEqual([inp], k3net.choose_inn(['1.1.1.1', inp])) + self.assertEqual([inp], k3net.choose_inn([inp, "1.1.1.1"])) + self.assertEqual([inp], k3net.choose_inn(["1.1.1.1", inp])) - self.assertEqual([inp], k3net.choose_ips([inp, '1.1.1.1'], k3net.INN)) - self.assertEqual([inp], k3net.choose_ips(['1.1.1.1', inp], k3net.INN)) - self.assertEqual([inp, '1.1.1.1'], k3net.choose_ips([inp, '1.1.1.1'])) - self.assertEqual(['1.1.1.1', inp], k3net.choose_ips(['1.1.1.1', inp])) + self.assertEqual([inp], k3net.choose_ips([inp, "1.1.1.1"], k3net.INN)) + self.assertEqual([inp], k3net.choose_ips(["1.1.1.1", inp], k3net.INN)) + self.assertEqual([inp, "1.1.1.1"], k3net.choose_ips([inp, "1.1.1.1"])) + self.assertEqual(["1.1.1.1", inp], k3net.choose_ips(["1.1.1.1", inp])) def test_ips_prefer(self): - cases = ( ([], k3net.PUB, []), ([], k3net.INN, []), - - (['1.2.3.4'], k3net.PUB, ['1.2.3.4']), - (['1.2.3.4'], k3net.INN, ['1.2.3.4']), - - (['172.16.0.1'], k3net.PUB, ['172.16.0.1']), - (['172.16.0.1'], k3net.INN, ['172.16.0.1']), - - (['172.16.0.1', '1.2.3.4'], k3net.PUB, ['1.2.3.4', '172.16.0.1']), - (['172.16.0.1', '1.2.3.4'], k3net.INN, ['172.16.0.1', '1.2.3.4']), - - (['1.2.3.4', '172.16.0.1'], k3net.PUB, ['1.2.3.4', '172.16.0.1']), - (['1.2.3.4', '172.16.0.1'], k3net.INN, ['172.16.0.1', '1.2.3.4']), + (["1.2.3.4"], k3net.PUB, ["1.2.3.4"]), + (["1.2.3.4"], k3net.INN, ["1.2.3.4"]), + (["172.16.0.1"], k3net.PUB, ["172.16.0.1"]), + (["172.16.0.1"], k3net.INN, ["172.16.0.1"]), + (["172.16.0.1", "1.2.3.4"], k3net.PUB, ["1.2.3.4", "172.16.0.1"]), + (["172.16.0.1", "1.2.3.4"], k3net.INN, ["172.16.0.1", "1.2.3.4"]), + (["1.2.3.4", "172.16.0.1"], k3net.PUB, ["1.2.3.4", "172.16.0.1"]), + (["1.2.3.4", "172.16.0.1"], k3net.INN, ["172.16.0.1", "1.2.3.4"]), ) for inp_ips, inp_class, outp in cases: @@ -203,117 +181,95 @@ def test_ips_prefer(self): def test_ips_prefer_by_idc(self): cases = ( - ('a', 'a', [], []), - ('a', 'a', ['1.1.1.1'], ['1.1.1.1']), - ('a', 'a', ['172.16.0.0'], ['172.16.0.0']), - ('a', 'a', ['172.16.0.0', '1.1.1.1'], ['172.16.0.0', '1.1.1.1']), - ('a', 'a', ['1.1.1.1', '172.16.0.0'], ['172.16.0.0', '1.1.1.1']), - - ('a', 'b', [], []), - ('a', 'b', ['1.1.1.1'], ['1.1.1.1']), - ('a', 'b', ['172.16.0.0'], ['172.16.0.0']), - ('a', 'b', ['172.16.0.0', '1.1.1.1'], ['1.1.1.1', '172.16.0.0']), - ('a', 'b', ['1.1.1.1', '172.16.0.0'], ['1.1.1.1', '172.16.0.0']), + ("a", "a", [], []), + ("a", "a", ["1.1.1.1"], ["1.1.1.1"]), + ("a", "a", ["172.16.0.0"], ["172.16.0.0"]), + ("a", "a", ["172.16.0.0", "1.1.1.1"], ["172.16.0.0", "1.1.1.1"]), + ("a", "a", ["1.1.1.1", "172.16.0.0"], ["172.16.0.0", "1.1.1.1"]), + ("a", "b", [], []), + ("a", "b", ["1.1.1.1"], ["1.1.1.1"]), + ("a", "b", ["172.16.0.0"], ["172.16.0.0"]), + ("a", "b", ["172.16.0.0", "1.1.1.1"], ["1.1.1.1", "172.16.0.0"]), + ("a", "b", ["1.1.1.1", "172.16.0.0"], ["1.1.1.1", "172.16.0.0"]), ) for idc_a, idc_b, ips, outp in cases: self.assertEqual(outp, k3net.choose_by_idc(idc_a, idc_b, ips)) def test_get_host_ip4(self): - - ips = k3net.get_host_ip4(iface_prefix='') + ips = k3net.get_host_ip4(iface_prefix="") self.assertNotEqual([], ips) for ip in ips: self.assertIsInstance(ip, str) self.assertTrue(k3net.is_ip4(ip)) - ips2 = k3net.get_host_ip4(exclude_prefix='') - self.assertEqual([], ips2, 'exclude any') + ips2 = k3net.get_host_ip4(exclude_prefix="") + self.assertEqual([], ips2, "exclude any") - self.assertEqual(ips, k3net.get_host_ip4( - exclude_prefix=[]), 'exclude nothing') + self.assertEqual(ips, k3net.get_host_ip4(exclude_prefix=[]), "exclude nothing") - self.assertEqual(ips, k3net.get_host_ip4( - exclude_prefix=None), 'exclude nothing') + self.assertEqual(ips, k3net.get_host_ip4(exclude_prefix=None), "exclude nothing") def test_get_host_devices(self): # TODO can not test - k3net.get_host_devices(iface_prefix='') + k3net.get_host_devices(iface_prefix="") def test_parse_ip_regex_str(self): - cases = ( - ('1.2.3.4', ['1.2.3.4']), - ('1.2.3.4,127.0.', ['1.2.3.4', '127.0.']), - ('-1.2.3.4,127.0.', [('1.2.3.4', False), '127.0.']), - ('-1.2.3.4,-127.0.', [('1.2.3.4', False), ('127.0.', False)]), + ("1.2.3.4", ["1.2.3.4"]), + ("1.2.3.4,127.0.", ["1.2.3.4", "127.0."]), + ("-1.2.3.4,127.0.", [("1.2.3.4", False), "127.0."]), + ("-1.2.3.4,-127.0.", [("1.2.3.4", False), ("127.0.", False)]), ) for inp, outp in cases: self.assertEqual(outp, k3net.parse_ip_regex_str(inp)) cases_err = ( - '', - ',', - ' , ', - '1,', - ',1', - '-1,', - ',-1', - '127,-', - '-,127', + "", + ",", + " , ", + "1,", + ",1", + "-1,", + ",-1", + "127,-", + "-,127", ) for inp in cases_err: - - dd('should fail with: ', repr(inp)) + dd("should fail with: ", repr(inp)) try: k3net.parse_ip_regex_str(inp) - self.fail('should fail with ' + repr(inp)) + self.fail("should fail with " + repr(inp)) except ValueError: pass def test_choose_ips_regex(self): cases = ( - (['127.0.0.1', '192.168.0.1'], ['127[.]'], - ['127.0.0.1']), - - (['127.0.0.1', '192.168.0.1'], ['2'], - []), - - (['127.0.0.1', '192.168.0.1'], ['[.]'], - []), - - (['127.0.0.1', '192.168.0.1'], ['1'], - ['127.0.0.1', '192.168.0.1']), - + (["127.0.0.1", "192.168.0.1"], ["127[.]"], ["127.0.0.1"]), + (["127.0.0.1", "192.168.0.1"], ["2"], []), + (["127.0.0.1", "192.168.0.1"], ["[.]"], []), + (["127.0.0.1", "192.168.0.1"], ["1"], ["127.0.0.1", "192.168.0.1"]), # negative match - (['127.0.0.1', '192.168.0.1'], [('1', False)], - []), - - (['127.0.0.1', '192.168.0.1'], [('127', False), ('192', False)], - []), - - (['127.0.0.1', '192.168.0.1'], [('12', False)], - ['192.168.0.1']), - - (['127.0.0.1', '192.168.0.1'], ['22', ('12', False)], - []), + (["127.0.0.1", "192.168.0.1"], [("1", False)], []), + (["127.0.0.1", "192.168.0.1"], [("127", False), ("192", False)], []), + (["127.0.0.1", "192.168.0.1"], [("12", False)], ["192.168.0.1"]), + (["127.0.0.1", "192.168.0.1"], ["22", ("12", False)], []), ) for ips, regs, outp in cases: - dd('case: ', ips, regs, outp) + dd("case: ", ips, regs, outp) self.assertEqual(outp, k3net.choose_by_regex(ips, regs)) def test_ip_interconvert_num(self): - cases_ip4_and_ip4_num = ( - ('127.0.0.1', 0x7f000001), - ('124.51.31.23', 0x7c331f17), - ('255.255.255.255', 0xffffffff), - ('1.2.3.4', 0x01020304), - ('5.6.7.8', 0x05060708), + ("127.0.0.1", 0x7F000001), + ("124.51.31.23", 0x7C331F17), + ("255.255.255.255", 0xFFFFFFFF), + ("1.2.3.4", 0x01020304), + ("5.6.7.8", 0x05060708), ) for ips, out in cases_ip4_and_ip4_num: @@ -326,44 +282,39 @@ def test_ip_interconvert_num(self): None, True, False, - '', - '1', + "", + "1", (), [], {}, - '1.', - '1.1', - '1.1.', - '1.1.1', - '1.1.1.', - - '.1.1.1', - - 'x.1.1.1', - '1.x.1.1', - '1.1.x.1', - '1.1.1.x', - - '1.1.1.1.', - '.1.1.1.1', - '1:1.1.1', - '1:1:1.1', - - '256.1.1.1', - '1.256.1.1', - '1.1.256.1', - '1.1.1.256', - - '1.1.1.1.', - '1.1.1.1.1', - '1.1.1.1.1.', - '1.1.1.1.1.1', + "1.", + "1.1", + "1.1.", + "1.1.1", + "1.1.1.", + ".1.1.1", + "x.1.1.1", + "1.x.1.1", + "1.1.x.1", + "1.1.1.x", + "1.1.1.1.", + ".1.1.1.1", + "1:1.1.1", + "1:1:1.1", + "256.1.1.1", + "1.256.1.1", + "1.1.256.1", + "1.1.1.256", + "1.1.1.1.", + "1.1.1.1.1", + "1.1.1.1.1.", + "1.1.1.1.1.1", -10, -100, -110000000000, 68719476735, - 'dada', - 'mu', + "dada", + "mu", 1099511627775, 1.3, 20.5, @@ -385,18 +336,18 @@ def test_ip_interconvert_num(self): def test_iner_ip_patterns(self): old = k3net.net.inner_ip_patterns - k3net.net.inner_ip_patterns = ['^172[.]18[.]2[.](3[2-9])$', '^172[.]18[.]2[.](4[0-7])$'] + k3net.net.inner_ip_patterns = ["^172[.]18[.]2[.](3[2-9])$", "^172[.]18[.]2[.](4[0-7])$"] case_inner_ip_true = ( - '172.18.2.32', - '172.18.2.37', - '172.18.2.47', + "172.18.2.32", + "172.18.2.37", + "172.18.2.47", ) case_inner_ip_false = ( - '172.18.2.31', - '172.18.2.48', - '172.18.2.49', + "172.18.2.31", + "172.18.2.48", + "172.18.2.49", ) for inp in case_inner_ip_true: