feat: initial p2pkg tool with CI, ruff, and stable tagging
Some checks failed
CI (tests + ruff) and stable tag / unittest (py3.10) (push) Has been cancelled
CI (tests + ruff) and stable tag / unittest (py3.11) (push) Has been cancelled
CI (tests + ruff) and stable tag / unittest (py3.12) (push) Has been cancelled
CI (tests + ruff) and stable tag / unittest (py3.13) (push) Has been cancelled
CI (tests + ruff) and stable tag / ruff (py3.12) (push) Has been cancelled
CI (tests + ruff) and stable tag / Tag stable (if version commit) (push) Has been cancelled
Some checks failed
CI (tests + ruff) and stable tag / unittest (py3.10) (push) Has been cancelled
CI (tests + ruff) and stable tag / unittest (py3.11) (push) Has been cancelled
CI (tests + ruff) and stable tag / unittest (py3.12) (push) Has been cancelled
CI (tests + ruff) and stable tag / unittest (py3.13) (push) Has been cancelled
CI (tests + ruff) and stable tag / ruff (py3.12) (push) Has been cancelled
CI (tests + ruff) and stable tag / Tag stable (if version commit) (push) Has been cancelled
https://chatgpt.com/share/69468609-0584-800f-a3e0-9d58210fb0e8
This commit is contained in:
106
.github/workflows/ci-and-stable.yml
vendored
Normal file
106
.github/workflows/ci-and-stable.yml
vendored
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
name: CI (tests + ruff) and stable tag
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["main"]
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
name: unittest (py${{ matrix.python-version }})
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
cache: pip
|
||||||
|
|
||||||
|
- name: Install (editable)
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install -e .
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: |
|
||||||
|
make test
|
||||||
|
|
||||||
|
ruff:
|
||||||
|
name: ruff (py3.12)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.12"
|
||||||
|
cache: pip
|
||||||
|
|
||||||
|
- name: Install (editable) + ruff
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install -e .
|
||||||
|
pip install ruff
|
||||||
|
|
||||||
|
- name: Ruff check
|
||||||
|
run: |
|
||||||
|
ruff check .
|
||||||
|
|
||||||
|
# Optional: falls du ruff format nutzt
|
||||||
|
# - name: Ruff format (check)
|
||||||
|
# run: |
|
||||||
|
# ruff format --check .
|
||||||
|
|
||||||
|
stable:
|
||||||
|
name: Tag stable (if version commit)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [test, ruff]
|
||||||
|
if: >
|
||||||
|
github.event_name == 'push' &&
|
||||||
|
github.ref == 'refs/heads/main' &&
|
||||||
|
github.repository_owner == 'kevinveenbirkenbach'
|
||||||
|
steps:
|
||||||
|
- name: Checkout (full history for tags)
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Check commit message for version
|
||||||
|
id: version
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
msg="$(git log -1 --pretty=%B)"
|
||||||
|
echo "Commit message:"
|
||||||
|
echo "$msg"
|
||||||
|
if echo "$msg" | grep -Eq '(^|[^0-9])(v?[0-9]+\.[0-9]+\.[0-9]+)([^0-9]|$)'; then
|
||||||
|
echo "has_version=true" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "has_version=false" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Update stable tag
|
||||||
|
if: steps.version.outputs.has_version == 'true'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
|
||||||
|
# Force-update lightweight tag "stable" to current commit
|
||||||
|
git tag -f stable
|
||||||
|
|
||||||
|
# Push (force) the tag
|
||||||
|
git push -f origin stable
|
||||||
22
LICENSE
22
LICENSE
@@ -1 +1,21 @@
|
|||||||
All rights reserved by Kevin Veen-Birkenbach
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 Kevin Veen-Birkenbach
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|||||||
4
MIRRORS
Normal file
4
MIRRORS
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
https://pypi.org/project/p2pkg/
|
||||||
|
ssh://git@git.veen.world:2201/kevinveenbirkenbach/p2pkg.git
|
||||||
|
ssh://git@code.infinito.nexus:2201/kevinveenbirkenbach/p2pkg.git
|
||||||
|
git@github.com:kevinveenbirkenbach/p2pkg.git
|
||||||
20
Makefile
Normal file
20
Makefile
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
SHELL := /usr/bin/env bash
|
||||||
|
|
||||||
|
PYTHON ?= python3
|
||||||
|
PIP ?= $(PYTHON) -m pip
|
||||||
|
|
||||||
|
.PHONY: venv install test clean
|
||||||
|
|
||||||
|
venv:
|
||||||
|
$(PYTHON) -m venv .venv
|
||||||
|
@echo "Activate with: . .venv/bin/activate"
|
||||||
|
|
||||||
|
install:
|
||||||
|
$(PIP) install -e .
|
||||||
|
|
||||||
|
test:
|
||||||
|
$(PYTHON) -m unittest discover -s tests -p "test_*.py" -v
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf .venv .pytest_cache .ruff_cache dist build *.egg-info
|
||||||
|
find . -name "__pycache__" -type d -prune -exec rm -rf {} +
|
||||||
61
README.md
61
README.md
@@ -1,6 +1,61 @@
|
|||||||
# p2pkg
|
# p2pkg
|
||||||
|
|
||||||
Homepage: https://git.veen.world/kevinveenbirkenbach/p2pkg
|
A small, purpose-built repository for a very specific migration:
|
||||||
|
|
||||||
## Author
|
- `foo.py` ➜ `foo/__main__.py`
|
||||||
Kevin Veen-Birkenbach <kevin@veen.world>
|
- Generates `foo/__init__.py` that re-exports the public API from `__main__`
|
||||||
|
so existing code like `import foo` or `from foo import some_function` keeps working.
|
||||||
|
- Keeps the original module code *as-is* in `__main__.py` (one-off refactor helper).
|
||||||
|
|
||||||
|
## Install (editable)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m venv .venv
|
||||||
|
. .venv/bin/activate
|
||||||
|
pip install -e .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Migrate one or more flat modules into packages
|
||||||
|
p2pkg roles_list.py another_module.py
|
||||||
|
|
||||||
|
# Or run directly
|
||||||
|
python tools/p2pkg.py roles_list.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Behavior
|
||||||
|
|
||||||
|
Given `roles_list.py`:
|
||||||
|
|
||||||
|
```
|
||||||
|
roles_list.py
|
||||||
|
```
|
||||||
|
|
||||||
|
After migration:
|
||||||
|
|
||||||
|
```
|
||||||
|
roles_list/
|
||||||
|
├── __init__.py # re-exports public API from __main__
|
||||||
|
└── __main__.py # contains the original implementation (moved)
|
||||||
|
```
|
||||||
|
|
||||||
|
- Running `python -m roles_list` executes `roles_list/__main__.py`.
|
||||||
|
- Existing imports remain compatible (via re-exports in `__init__.py`).
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
Run tests:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make test
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT License. See `LICENSE`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Author: Kevin Veen-Birkenbach
|
||||||
|
|||||||
11
flake.nix
11
flake.nix
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
description = "p2pkg";
|
|
||||||
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
|
||||||
outputs = { self, nixpkgs }:
|
|
||||||
let system = "x86_64-linux"; pkgs = import nixpkgs { inherit system; };
|
|
||||||
in {
|
|
||||||
devShells.${system}.default = pkgs.mkShell {
|
|
||||||
packages = with pkgs; [ python312 python312Packages.pytest python312Packages.ruff ];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,21 +1,38 @@
|
|||||||
[build-system]
|
[build-system]
|
||||||
requires = ["setuptools>=68", "wheel"]
|
requires = ["hatchling>=1.24.2"]
|
||||||
build-backend = "setuptools.build_meta"
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "p2pkg"
|
name = "p2pkg"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = ""
|
description = "One-off refactor helper: migrate foo.py -> foo/__main__.py and generate foo/__init__.py that re-exports public API."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
authors = [{ name = "Kevin Veen-Birkenbach", email = "kevin@veen.world" }]
|
license = { file = "LICENSE" }
|
||||||
license = { text = "All rights reserved by Kevin Veen-Birkenbach" }
|
authors = [
|
||||||
urls = { Homepage = "https://git.veen.world/kevinveenbirkenbach/p2pkg" }
|
{ name = "Kevin Veen-Birkenbach" }
|
||||||
|
]
|
||||||
|
keywords = ["refactor", "python", "imports", "package"]
|
||||||
|
classifiers = [
|
||||||
|
"License :: OSI Approved :: MIT License",
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"Programming Language :: Python :: 3 :: Only",
|
||||||
|
"Topic :: Software Development :: Refactoring"
|
||||||
|
]
|
||||||
|
|
||||||
dependencies = []
|
[project.scripts]
|
||||||
|
p2pkg = "p2pkg.__main__:main"
|
||||||
|
|
||||||
[tool.setuptools]
|
[tool.hatch.build.targets.wheel]
|
||||||
package-dir = {"" = "src"}
|
packages = ["src/p2pkg"]
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
[tool.hatch.build.targets.sdist]
|
||||||
where = ["src"]
|
include = [
|
||||||
|
"/src",
|
||||||
|
"/tests",
|
||||||
|
"/tools",
|
||||||
|
"/README.md",
|
||||||
|
"/LICENSE",
|
||||||
|
"/Makefile",
|
||||||
|
"/pyproject.toml",
|
||||||
|
]
|
||||||
|
|||||||
1
src/p2pkg/__init__.py
Normal file
1
src/p2pkg/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""p2pkg: migrate flat modules into packages with __main__.py."""
|
||||||
112
src/p2pkg/__main__.py
Normal file
112
src/p2pkg/__main__.py
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import pathlib
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
|
||||||
|
INIT_TEMPLATE = """\
|
||||||
|
\"\"\"Compatibility wrapper.
|
||||||
|
|
||||||
|
This package was migrated from a flat module ({name}.py) to a package layout:
|
||||||
|
{name}/__main__.py contains the original implementation.
|
||||||
|
|
||||||
|
We re-export the public API so existing imports keep working.
|
||||||
|
\"\"\"
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from . import __main__ as _main
|
||||||
|
from .__main__ import * # noqa: F401,F403
|
||||||
|
|
||||||
|
# Prefer explicit __all__ if the original module defined it.
|
||||||
|
__all__ = getattr(_main, "__all__", [n for n in dir(_main) if not n.startswith("_")])
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def _run(cmd: list[str], cwd: pathlib.Path | None = None) -> None:
|
||||||
|
subprocess.run(cmd, check=True, cwd=str(cwd) if cwd else None)
|
||||||
|
|
||||||
|
|
||||||
|
def _have_git_repo(root: pathlib.Path) -> bool:
|
||||||
|
try:
|
||||||
|
subprocess.run(
|
||||||
|
["git", "rev-parse", "--is-inside-work-tree"],
|
||||||
|
cwd=str(root),
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _git_mv(src: pathlib.Path, dst: pathlib.Path, cwd: pathlib.Path) -> None:
|
||||||
|
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
_run(["git", "mv", str(src), str(dst)], cwd=cwd)
|
||||||
|
|
||||||
|
|
||||||
|
def _fs_mv(src: pathlib.Path, dst: pathlib.Path) -> None:
|
||||||
|
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
src.rename(dst)
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_one(py_file: pathlib.Path, use_git: bool, repo_root: pathlib.Path) -> None:
|
||||||
|
"""Migrate a single flat module file (foo.py) into a package (foo/__main__.py)."""
|
||||||
|
py_file = py_file.resolve()
|
||||||
|
if py_file.suffix != ".py":
|
||||||
|
raise ValueError(f"Not a .py file: {py_file}")
|
||||||
|
|
||||||
|
name = py_file.stem
|
||||||
|
pkg_dir = py_file.parent / name
|
||||||
|
|
||||||
|
if pkg_dir.exists() and not pkg_dir.is_dir():
|
||||||
|
raise RuntimeError(f"Target exists but is not a directory: {pkg_dir}")
|
||||||
|
|
||||||
|
target_main = pkg_dir / "__main__.py"
|
||||||
|
target_init = pkg_dir / "__init__.py"
|
||||||
|
|
||||||
|
if target_main.exists():
|
||||||
|
raise RuntimeError(f"Refusing to overwrite existing: {target_main}")
|
||||||
|
|
||||||
|
if use_git:
|
||||||
|
_git_mv(py_file, target_main, cwd=repo_root)
|
||||||
|
else:
|
||||||
|
_fs_mv(py_file, target_main)
|
||||||
|
|
||||||
|
if not target_init.exists():
|
||||||
|
target_init.write_text(INIT_TEMPLATE.format(name=name), encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str] | None = None) -> int:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
prog="p2pkg",
|
||||||
|
description="Migrate foo.py -> foo/__main__.py and generate foo/__init__.py that re-exports public API.",
|
||||||
|
)
|
||||||
|
parser.add_argument("files", nargs="+", help="Python module files to migrate (e.g. roles_list.py other.py).")
|
||||||
|
parser.add_argument("--no-git", action="store_true", help="Do not use `git mv` even if inside a git repo.")
|
||||||
|
parser.add_argument(
|
||||||
|
"--repo-root",
|
||||||
|
default=".",
|
||||||
|
help="Repository root used for git operations (default: current working directory).",
|
||||||
|
)
|
||||||
|
|
||||||
|
ns = parser.parse_args(argv)
|
||||||
|
repo_root = pathlib.Path(ns.repo_root).resolve()
|
||||||
|
use_git = (not ns.no_git) and _have_git_repo(repo_root)
|
||||||
|
|
||||||
|
for f in ns.files:
|
||||||
|
path = pathlib.Path(f)
|
||||||
|
if not path.is_absolute():
|
||||||
|
path = (repo_root / path).resolve()
|
||||||
|
if not path.exists():
|
||||||
|
raise FileNotFoundError(str(path))
|
||||||
|
migrate_one(path, use_git=use_git, repo_root=repo_root)
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
91
tests/test_migration.py
Normal file
91
tests/test_migration.py
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib.util
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import textwrap
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from p2pkg.__main__ import migrate_one
|
||||||
|
|
||||||
|
|
||||||
|
class TestMigration(unittest.TestCase):
|
||||||
|
def test_migrate_creates_package_and_exports_public_api(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as td:
|
||||||
|
root = Path(td)
|
||||||
|
mod = root / "roles_list.py"
|
||||||
|
mod.write_text(textwrap.dedent("""\
|
||||||
|
__all__ = ["add", "PUBLIC_CONST"]
|
||||||
|
PUBLIC_CONST = 123
|
||||||
|
_PRIVATE_CONST = 999
|
||||||
|
|
||||||
|
def add(a: int, b: int) -> int:
|
||||||
|
return a + b
|
||||||
|
|
||||||
|
def _hidden() -> int:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print("running as script")
|
||||||
|
"""),encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
migrate_one(mod, use_git=False, repo_root=root)
|
||||||
|
|
||||||
|
pkg = root / "roles_list"
|
||||||
|
self.assertTrue((pkg / "__main__.py").exists())
|
||||||
|
self.assertTrue((pkg / "__init__.py").exists())
|
||||||
|
self.assertFalse(mod.exists())
|
||||||
|
|
||||||
|
# Import the package and ensure re-exports work
|
||||||
|
sys.path.insert(0, str(root))
|
||||||
|
try:
|
||||||
|
import roles_list # type: ignore
|
||||||
|
|
||||||
|
self.assertTrue(hasattr(roles_list, "add"))
|
||||||
|
self.assertEqual(roles_list.add(2, 3), 5)
|
||||||
|
self.assertTrue(hasattr(roles_list, "PUBLIC_CONST"))
|
||||||
|
self.assertEqual(roles_list.PUBLIC_CONST, 123)
|
||||||
|
|
||||||
|
# __all__ should be preserved
|
||||||
|
self.assertEqual(set(roles_list.__all__), {"add", "PUBLIC_CONST"})
|
||||||
|
self.assertFalse(hasattr(roles_list, "_hidden"))
|
||||||
|
finally:
|
||||||
|
sys.path.remove(str(root))
|
||||||
|
if "roles_list" in sys.modules:
|
||||||
|
del sys.modules["roles_list"]
|
||||||
|
|
||||||
|
def test_migrate_without_explicit_all_exports_public_names(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as td:
|
||||||
|
root = Path(td)
|
||||||
|
mod = root / "foo.py"
|
||||||
|
mod.write_text(textwrap.dedent("""\
|
||||||
|
VALUE = "ok"
|
||||||
|
|
||||||
|
def hello() -> str:
|
||||||
|
return "hi"
|
||||||
|
|
||||||
|
def _private() -> str:
|
||||||
|
return "no"
|
||||||
|
"""), encoding="utf-8")
|
||||||
|
migrate_one(mod, use_git=False, repo_root=root)
|
||||||
|
|
||||||
|
sys.path.insert(0, str(root))
|
||||||
|
try:
|
||||||
|
import foo # type: ignore
|
||||||
|
|
||||||
|
self.assertEqual(foo.hello(), "hi")
|
||||||
|
self.assertEqual(foo.VALUE, "ok")
|
||||||
|
self.assertIn("hello", foo.__all__)
|
||||||
|
self.assertIn("VALUE", foo.__all__)
|
||||||
|
self.assertNotIn("_private", foo.__all__)
|
||||||
|
finally:
|
||||||
|
sys.path.remove(str(root))
|
||||||
|
if "foo" in sys.modules:
|
||||||
|
del sys.modules["foo"]
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main(verbosity=2)
|
||||||
Reference in New Issue
Block a user