Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
115f480
docs: update README to match CLI and tag implementation
Bonajo Sep 21, 2026
70d2b86
fix: make -b/--binary and -u/--unknown work on the command line
Bonajo Sep 21, 2026
0f723ef
fix: continue with remaining files when FAIL is hit for unknown exten…
Bonajo Sep 21, 2026
80bc770
fix: build uncomment regex per comment style and remove close symbol
Bonajo Sep 21, 2026
bfb0078
fix: print dry run output to stdout instead of logging it
Bonajo Sep 21, 2026
aca719b
fix: validate -c option, lowercase extension and stop mutating global…
Bonajo Sep 21, 2026
77af27b
fix: resolve working directory before checking it is inside the curre…
Bonajo Sep 21, 2026
17fb0e9
fix: do not change the current directory when matching files
Bonajo Sep 21, 2026
e4ff338
fix: avoid duplicate log handlers and clamp verbosity to debug
Bonajo Sep 21, 2026
0b0498b
fix: report line and reason for invalid range tags and raise StripError
Bonajo Sep 21, 2026
3267d02
fix: only match cs:ignore as a whole tag
Bonajo Sep 21, 2026
bd22142
fix: read and write files as utf-8
Bonajo Sep 21, 2026
77f047d
build: switch from Poetry to uv and require Python 3.10
Bonajo Sep 21, 2026
6b13dee
fix: fail on error by default in strip_files, like the command line
Bonajo Sep 21, 2026
4589f9a
fix: do not strip files from the output directory again
Bonajo Sep 21, 2026
e29102e
refactor: keep tokenizer state per instance
Bonajo Sep 21, 2026
e17c3b9
chore: add ruff linter and remove unused imports and dead code
Bonajo Sep 21, 2026
0f5fa8f
fix: include the tags in the tokenizer cache key
Bonajo Sep 21, 2026
cf73bdd
build: bump pytest to 9.1.1 to fix the /tmp directory vulnerability
Bonajo Sep 21, 2026
d74beac
fix: do not copy binary files during a dry run
Bonajo Sep 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 7 additions & 21 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,28 +15,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
- name: Install uv
uses: astral-sh/setup-uv@v10.1.0
with:
python-version: ${{ matrix.python-version }}
- name: Install Poetry
uses: snok/install-poetry@v1
with:
virtualenvs-create: true
virtualenvs-in-project: true
installer-parallel: true
- name: Load cached venv
id: cached-poetry-venv
uses: actions/cache@v4
with:
path: .venv
key: venv-${{ matrix.python-version }}-${{ hashFiles('poetry.lock') }}
- name: Install dependencies
if: steps.cached-poetry-venv.outputs.cache-hit != 'true'
run: poetry install
enable-cache: true
- name: Build
run: poetry build
run: uv build
- name: Publish
run: |
poetry config pypi-token.pypi ${{ secrets.PYPI_TOKEN }}
poetry publish
run: uv publish
env:
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN }}
72 changes: 17 additions & 55 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,29 +20,15 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
- name: Install uv
uses: astral-sh/setup-uv@v10.1.0
with:
python-version: ${{ matrix.python-version }}
- name: Install Poetry
uses: snok/install-poetry@76e04a911780d5b312d89783f7b1cd627778900a
with:
virtualenvs-create: true
virtualenvs-in-project: true
installer-parallel: true
- name: Load cached venv
id: cached-poetry-venv
uses: actions/cache@v4
with:
path: .venv
key: venv-${{ matrix.python-version }}-${{ runner.os }}-${{ hashFiles('poetry.lock') }}
enable-cache: true
- name: Install dependencies
if: steps.cached-poetry-venv.outputs.cache-hit != 'true'
run: poetry install --no-interaction
run: uv sync --locked
- name: Run tests
run: |
source $VENV
poetry run pytest --cov --cov-report=xml --junitxml=tests.xml
run: uv run pytest --cov --cov-report=xml --junitxml=tests.xml
- name: Test Report
uses: dorny/test-reporter@v1
if: success() || failure()
Expand All @@ -57,28 +43,16 @@ jobs:
python-version: [ "3.12" ]
runs-on: "ubuntu-latest"
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v10.1.0
with:
python-version: ${{ matrix.python-version }}
- name: Install Poetry
uses: snok/install-poetry@v1
with:
virtualenvs-create: true
virtualenvs-in-project: true
installer-parallel: true
- name: Load cached venv
id: cached-poetry-venv
uses: actions/cache@v4
with:
path: .venv
key: venv-${{ matrix.python-version }}-{{ runner.os }}-${{ hashFiles('poetry.lock') }}
enable-cache: true
- name: Install dependencies
if: steps.cached-poetry-venv.outputs.cache-hit != 'true'
run: poetry install --no-interaction
run: uv sync --locked
- name: Run tests
run: poetry run pytest --cov --cov-report=xml --junitxml=tests.xml
run: uv run pytest --cov --cov-report=xml --junitxml=tests.xml
- name: Code Coverage Report
uses: irongut/CodeCoverageSummary@v1.3.0
with:
Expand All @@ -99,25 +73,13 @@ jobs:
python-version: [ "3.12" ]
runs-on: "ubuntu-latest"
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v10.1.0
with:
python-version: ${{ matrix.python-version }}
- name: Install Poetry
uses: snok/install-poetry@v1
with:
virtualenvs-create: true
virtualenvs-in-project: true
installer-parallel: true
- name: Load cached venv
id: cached-poetry-venv
uses: actions/cache@v3
with:
path: .venv
key: venv-${{ matrix.python-version }}-{{ runner.os }}-${{ hashFiles('poetry.lock') }}
enable-cache: true
- name: Install dependencies
if: steps.cached-poetry-venv.outputs.cache-hit != 'true'
run: poetry install --no-interaction
run: uv sync --locked
- name: Type checker
run: poetry run mypy codestripper
run: uv run mypy codestripper
53 changes: 41 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,41 @@ CodeStripper can be used as a Python Module and as a command line tool. The comm
| Flag | Long form | Description | Default value | Required |
|----------|------|-------------|---------------|----------|
| `<positional>` | None | files to include for code stripping (glob) | None | True |
| -e | --exclude | files to exclude for code stripping (glob) | None | False |
| -c | --comment | comment symbol(s) for the given language | // | False |
| -x | --exclude | files to exclude for code stripping (glob), can be repeated | None | False |
| -c | --comment | comment symbol(s) for a file extension, format `<extension>:<open>` or `<extension>:<open>:<close>` (e.g. `.java://`), the extension starts with a `.` and is case-insensitive, can be repeated | built-in mapping (see below) | False |
| -o | --output | the output directory to store the stripped files | out | False |
| -r | --recursive | do NOT use recursive globs for include/exclude | True | False |
| -v | --verbosity | increase output verbosity | None | False |
| -d | --dry | execute a dry run | False | False |
| -v | --verbosity | increase output verbosity (can be repeated) | 0 | False |
| -d | --dry-run | execute a dry run, prints the stripped files (with a `==> <file> <==` header) to stdout instead of writing them | False | False |
| -w | --working-directory | set the working directory for include/exclude | pwd | False |
| -e | --fail-on-error | do NOT fail if an error occurs during code stripping | fail on error | False |
| -b | --binary | what to do if a binary file is matched: `fail`, `ignore` or `include` | fail | False |
| -u | --unknown | what to do if a file with an unknown extension is matched: `fail`, `ignore` or `include` | fail | False |

### Supported comment styles

The comment symbol is chosen based on the file extension. The following extensions are supported by default:

| Comment | Extensions |
|---------|------------|
| `//` | `.java`, `.cs`, `.js`, `.php`, `.swift` |
| `#` | `.py`, `.r`, `.ps1`, `.rb`, `.yml`, `.yaml`, files without extension |
| `%` | `.tex`, `.m` |
| `--` | `.sql`, `.lua` |
| `<!-- -->` | `.xml` |
| `(* *)` | `.ml` |

Use `-c` to add or override an extension, for example `-c .kt://` or `-c .html:'<!--':'-->'`.

### Python module

```python
from codestripper.code_stripper import strip_files

stripped = strip_files(["src/Test.java"], working_directory=".", output="out", dry_run=False)
```

`strip_files` returns the files that were stripped. Files that fail (e.g. an invalid tag) are logged and the other files are still processed, after which a `StripError` is raised. Pass `fail_on_error=False` to only log the errors, like the `-e` flag of the command line tool.

## Examples

Expand Down Expand Up @@ -150,7 +178,6 @@ classDiagram
Tag <|-- RangeTag

class Tag{
<<Abstract>>
+offset: int
+start: int
+end: int
Expand All @@ -169,17 +196,18 @@ classDiagram
+SingleTag(data: TagData)
}
class RangeOpenTag{
+RangeOpen(parent: Type, data: TagData)
+RangeOpenTag(parent: Type, data: TagData)
}
class RangeCloseTag{
+RangeOpen(parent: Type, data: TagData)
+RangeCloseTag(parent: Type, data: TagData)
}
class RangeTag{
+inset: int
+start: int
+end: int
+open_tag: RangeOpenTag
+close_tag: RangeCloseTag
+tags: List[Tag]
+RangeTag(open_tag: RangeOpenTag, close_tag: RangeCloseTag)
+add_tags(tags: Iterable[Tag])
}
Expand Down Expand Up @@ -212,7 +240,7 @@ class TestTag(SingleTag):
# None means the line is removed

def is_valid(self) -> bool:
# Return wether the tag is valid
# Return whether the tag is valid
```

- RangeTag: Range needs a `RangeOpenTag`, `RangeCloseTag` and a `RangeTag`
Expand All @@ -228,7 +256,7 @@ class TestOpenTag(RangeOpenTag):
# None means the line is removed

def is_valid(self) -> bool:
# Return wether the tag is valid
# Return whether the tag is valid

class TestCloseTag(RangeCloseTag):
# Same as RangeOpenTag
Expand All @@ -242,15 +270,16 @@ class TestRangeTag(RangeTag):
def execute(self, content: str) -> Union[str, None]:
# Manipulate lines between the tags
```
3. Add the new tag(s) to the `default_tags` in the `tokenizer`,
3. Export the new tag(s) in `codestripper/tags/__init__.py`, import them in the `tokenizer` and add them to its `default_tags`,

```python
default_tags: Set[Type[SingleTag]] = {
IgnoreFileTag,
RemoveOpenTag,
...,
TestTag,
TestOpenTag
TestOpenTag,
TestCloseTag
}
```
> :warning: **Only the `SingleTag`(s) need to be added, not the `RangeTag`**
> :warning: **Only the `SingleTag`(s) (including `RangeOpenTag` and `RangeCloseTag`) need to be added, not the `RangeTag`**
1 change: 1 addition & 0 deletions codestripper/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from codestripper.code_stripper import CodeStripper, strip_files
from codestripper.errors import StripError
1 change: 0 additions & 1 deletion codestripper/__main__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import sys

from codestripper.cli import main

Expand Down
30 changes: 22 additions & 8 deletions codestripper/cli.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,30 @@
import argparse
import os
from typing import List

from codestripper.code_stripper import strip_files
from codestripper.utils import FileUtils, set_logger_level, get_working_directory
from codestripper.utils.comments import parse_comment
from codestripper.utils.enums import UnexpectedInputOptions


def comment_argument(value: str) -> str:
"""Argparse type that validates a comment specification, the specification itself is parsed later"""
try:
parse_comment(value)
except ValueError as error:
raise argparse.ArgumentTypeError(str(error))
return value


def add_commandline_arguments(parser: argparse.ArgumentParser) -> None:
"""Add command line arguments"""
# Add positional arguments
parser.add_argument("include", nargs="+", help="files to include for code stripping (multiple files or glob)")
# Add optional arguments
parser.add_argument("-x", "--exclude", action="append",
help="files to include for code stripping (glob)", default=[])
parser.add_argument("-c", "--comment", action="append",
help="comment symbol(s) for the given language, usage: <extension>:<comment> (e.g. .java://")
help="files to exclude for code stripping (glob)", default=[])
parser.add_argument("-c", "--comment", action="append", type=comment_argument,
help="comment symbol(s) for the given language, usage: <extension>:<open>[:<close>] (e.g. .java://)")
parser.add_argument("-v", "--verbosity", action="count", help="increase output verbosity", default=0)
parser.add_argument("-o", "--output", action="store",
help="output directory to store the stripped files", default="out")
Expand All @@ -27,9 +36,10 @@ def add_commandline_arguments(parser: argparse.ArgumentParser) -> None:
help="set the working directory for include/exclude", default=os.getcwd())
parser.add_argument("-e", "--fail-on-error", action="store_false",
help="Fail if an error occurs during code stripping")
parser.add_argument("-b", "--binary", choices=list(UnexpectedInputOptions), default=UnexpectedInputOptions.FAIL,
unexpected_choices = [option.value for option in UnexpectedInputOptions]
parser.add_argument("-b", "--binary", choices=unexpected_choices, default=UnexpectedInputOptions.FAIL.value,
action="store", help="What to do if binary file is matched")
parser.add_argument("-u", "--unknown", choices=list(UnexpectedInputOptions), default=UnexpectedInputOptions.FAIL,
parser.add_argument("-u", "--unknown", choices=unexpected_choices, default=UnexpectedInputOptions.FAIL.value,
action="store", help="What to do if a file with unknown extension is matched")


Expand All @@ -46,9 +56,13 @@ def main() -> None:
set_logger_level(logger_name, args.verbosity)

# Find the files, based on the command line arguments
cwd = get_working_directory(args.working_directory)
try:
cwd = get_working_directory(args.working_directory)
except ValueError as error:
parser.error(str(error))
files = FileUtils(args.include, args.exclude, cwd, args.recursive, logger_name).get_matching_files()
# Strip all the files

strip_files(files, cwd, comments=args.comment, output=args.output, dry_run=args.dry_run,
fail_on_error=args.fail_on_error)
fail_on_error=args.fail_on_error, binary=UnexpectedInputOptions(args.binary),
unknown_extension=UnexpectedInputOptions(args.unknown))
Loading
Loading