pdacek 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pdacek-0.1.0/.gitignore +26 -0
- pdacek-0.1.0/CHANGELOG.md +18 -0
- pdacek-0.1.0/LICENSE +21 -0
- pdacek-0.1.0/PKG-INFO +19 -0
- pdacek-0.1.0/PUBLISH.md +103 -0
- pdacek-0.1.0/README.md +6 -0
- pdacek-0.1.0/conftest.py +8 -0
- pdacek-0.1.0/examples/notebook_cells.py +44 -0
- pdacek-0.1.0/examples/run_all.py +15 -0
- pdacek-0.1.0/pyproject.toml +37 -0
- pdacek-0.1.0/src/pdacek/__init__.py +116 -0
- pdacek-0.1.0/src/pdacek/__main__.py +6 -0
- pdacek-0.1.0/src/pdacek/cli.py +98 -0
- pdacek-0.1.0/src/pdacek/compat.py +129 -0
- pdacek-0.1.0/src/pdacek/programs/__init__.py +107 -0
- pdacek-0.1.0/src/pdacek/programs/pl01_preprocessing.py +80 -0
- pdacek-0.1.0/src/pdacek/programs/pl02_ngrams.py +66 -0
- pdacek-0.1.0/src/pdacek/programs/pl03_edit_distance.py +89 -0
- pdacek-0.1.0/src/pdacek/programs/pl04_parsing.py +150 -0
- pdacek-0.1.0/src/pdacek/programs/pl05_naive_bayes.py +95 -0
- pdacek-0.1.0/src/pdacek/py.typed +0 -0
- pdacek-0.1.0/src/pdacek/scripts/pl01_preprocessing.py +62 -0
- pdacek-0.1.0/src/pdacek/scripts/pl02_ngrams.py +46 -0
- pdacek-0.1.0/src/pdacek/scripts/pl03_edit_distance.py +47 -0
- pdacek-0.1.0/src/pdacek/scripts/pl04_parsing.py +95 -0
- pdacek-0.1.0/src/pdacek/scripts/pl05_naive_bayes.py +53 -0
- pdacek-0.1.0/tests/test_package.py +104 -0
- pdacek-0.1.0/tests/test_programs.py +165 -0
- pdacek-0.1.0/tools/rename.py +84 -0
pdacek-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# build artefacts
|
|
2
|
+
build/
|
|
3
|
+
dist/
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.eggs/
|
|
6
|
+
|
|
7
|
+
# environments
|
|
8
|
+
.venv/
|
|
9
|
+
venv/
|
|
10
|
+
__pycache__/
|
|
11
|
+
*.py[cod]
|
|
12
|
+
|
|
13
|
+
# tooling
|
|
14
|
+
.pytest_cache/
|
|
15
|
+
.mypy_cache/
|
|
16
|
+
.ruff_cache/
|
|
17
|
+
.coverage
|
|
18
|
+
htmlcov/
|
|
19
|
+
|
|
20
|
+
# secrets — never commit a PyPI token
|
|
21
|
+
.pypirc
|
|
22
|
+
.pypi-token
|
|
23
|
+
|
|
24
|
+
# OS noise
|
|
25
|
+
.DS_Store
|
|
26
|
+
Thumbs.db
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here.
|
|
4
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
|
|
5
|
+
and the project uses [Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## [0.1.0] - 2026-11-15
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Five experiments: preprocessing pipeline, n-gram probabilities, minimum edit
|
|
12
|
+
distance, CFG parsing (top-down and bottom-up), add-one Naive Bayes.
|
|
13
|
+
- `pdacek.p1()` … `pdacek.p5()` shorthands for notebook use.
|
|
14
|
+
- `source(n)` / `save(n)` for standalone listings.
|
|
15
|
+
- `pdacek` console script and `python -m pdacek` entry point.
|
|
16
|
+
- Regex tokenizer and built-in stop list as offline fallbacks for missing NLTK
|
|
17
|
+
corpora.
|
|
18
|
+
- Test suite covering every experiment, the public API and the CLI.
|
pdacek-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sohel Havaldar
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
pdacek-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pdacek
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
License: MIT
|
|
5
|
+
License-File: LICENSE
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Requires-Dist: nltk>=3.8
|
|
8
|
+
Provides-Extra: dev
|
|
9
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
10
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
11
|
+
Requires-Dist: twine>=5.0; extra == 'dev'
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
import pdacek
|
|
15
|
+
pdacek.p1() # preprocessing pipeline
|
|
16
|
+
pdacek.p2() # n-gram probabilities
|
|
17
|
+
pdacek.p3() # minimum edit distance
|
|
18
|
+
pdacek.p4() # top-down / bottom-up parsing
|
|
19
|
+
pdacek.p5() # Naive Bayes
|
pdacek-0.1.0/PUBLISH.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Publishing checklist
|
|
2
|
+
|
|
3
|
+
Everything below is local until the upload step — nothing is sent anywhere
|
|
4
|
+
without a PyPI token.
|
|
5
|
+
|
|
6
|
+
## 1. The name
|
|
7
|
+
|
|
8
|
+
`pdacek` — checked free on PyPI on 2026-11-15 (a 404 from the JSON API). Kept short
|
|
9
|
+
on purpose: `import pdacek`, `pdacek.p1()`, `pdacek 3`. It is a valid identifier, not a
|
|
10
|
+
Python keyword, and does not shadow a standard-library module.
|
|
11
|
+
|
|
12
|
+
83 other two-letter names are also free; the full scan of all 676 combinations
|
|
13
|
+
is in `../two-letter-names/pypi-two-letter-names.csv`. If you ever want to
|
|
14
|
+
switch, one command rewrites `pyproject.toml`, the source directory, imports,
|
|
15
|
+
docs and tests:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
python tools/rename.py <new-name>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Then rebuild.
|
|
22
|
+
|
|
23
|
+
## 2. Check the name is still free
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
curl -s -o /dev/null -w "%{http_code}\n" https://pypi.org/pypi/<name>/json
|
|
27
|
+
# 404 = free, 200 = taken
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
PyPI also rejects names that only differ from an existing project by `-`, `_`
|
|
31
|
+
or `.`, so pick something that normalises differently from `plnlab`.
|
|
32
|
+
|
|
33
|
+
## 3. Build
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
python -m pip install -U build twine
|
|
37
|
+
python -m build
|
|
38
|
+
python -m twine check dist/*
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`dist/` should contain one `.whl` and one `.tar.gz`, both `PASSED`.
|
|
42
|
+
|
|
43
|
+
## 4. Rehearse on TestPyPI (recommended)
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
python -m twine upload --repository testpypi dist/*
|
|
47
|
+
python -m pip install --index-url https://test.pypi.org/simple/ \
|
|
48
|
+
--extra-index-url https://pypi.org/simple/ pdacek
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
A TestPyPI rehearsal catches metadata and filename problems without burning a
|
|
52
|
+
version number on the real index.
|
|
53
|
+
|
|
54
|
+
## 5. Publish for real
|
|
55
|
+
|
|
56
|
+
Create an API token at <https://pypi.org/manage/account/token/> (scope it to the
|
|
57
|
+
project after the first upload), then:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
export TWINE_USERNAME=__token__
|
|
61
|
+
export TWINE_PASSWORD=pypi-AgEIcHlwaS5vcmc... # paste the token
|
|
62
|
+
python -m twine upload dist/*
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Or pass it inline without leaving it in shell history:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
TWINE_USERNAME=__token__ TWINE_PASSWORD="$(cat ~/.pypi-token)" python -m twine upload dist/*
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Never commit the token; `~/.pypirc` with `chmod 600` is the usual alternative.
|
|
72
|
+
|
|
73
|
+
## 6. After the upload
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
pip install pdacek # or the name you chose
|
|
77
|
+
python -c "import pdacek; pdacek.p1()"
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The project page appears at `https://pypi.org/project/<name>/` within a minute
|
|
81
|
+
or two; the JSON API updates immediately.
|
|
82
|
+
|
|
83
|
+
## 7. Releasing again
|
|
84
|
+
|
|
85
|
+
1. Bump `version` in `pyproject.toml` and `__version__` in
|
|
86
|
+
`src/<name>/__init__.py` (keep them in sync — the test suite checks it).
|
|
87
|
+
2. Add a `CHANGELOG.md` entry.
|
|
88
|
+
3. `python -m build && python -m twine upload dist/*`.
|
|
89
|
+
|
|
90
|
+
Versions are immutable on PyPI: a published version can be yanked, not
|
|
91
|
+
replaced, so bump before every upload.
|
|
92
|
+
|
|
93
|
+
## Notes on what this package does at runtime
|
|
94
|
+
|
|
95
|
+
- No network calls, no telemetry, no file writes outside the paths you pass to
|
|
96
|
+
`save()`.
|
|
97
|
+
- `nltk.download(...)` is only attempted when the local corpus is missing, and
|
|
98
|
+
a failure silently falls back to the built-in regex tokenizer.
|
|
99
|
+
- `save(n)` writes `<slug>.py` in the directory you name (default: current).
|
|
100
|
+
|
|
101
|
+
Kept deliberately boring so the package stays a normal, inspectable library:
|
|
102
|
+
anyone running `pip download` and reading `src/` sees exactly the five lab
|
|
103
|
+
programs and nothing else.
|
pdacek-0.1.0/README.md
ADDED
pdacek-0.1.0/conftest.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""What a Colab/Jupyter session looks like, cell by cell.
|
|
2
|
+
|
|
3
|
+
Cell 1
|
|
4
|
+
------
|
|
5
|
+
%pip install -q pdacek
|
|
6
|
+
|
|
7
|
+
Cell 2
|
|
8
|
+
------
|
|
9
|
+
import pdacek
|
|
10
|
+
pdacek.programs()
|
|
11
|
+
|
|
12
|
+
Cell 3 - run one experiment inline
|
|
13
|
+
----------------------------------
|
|
14
|
+
pdacek.p3()
|
|
15
|
+
|
|
16
|
+
Cell 4 - or print the listing and keep editing it below
|
|
17
|
+
--------------------------------------------------------
|
|
18
|
+
pdacek.source(3)
|
|
19
|
+
|
|
20
|
+
Cell 5 - use the functions directly, no printing
|
|
21
|
+
------------------------------------------------
|
|
22
|
+
from pdacek import NaiveBayes
|
|
23
|
+
|
|
24
|
+
model = NaiveBayes().fit([
|
|
25
|
+
("fun, couple, love, love", "comedy"),
|
|
26
|
+
("fast, furious, shoot", "action"),
|
|
27
|
+
])
|
|
28
|
+
model.predict(["love", "fun"])
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from pdacek import NaiveBayes, Parser, min_edit_distance, preprocess
|
|
32
|
+
|
|
33
|
+
# Functions instead of printed output: handy when you want to grade results.
|
|
34
|
+
print(min_edit_distance("kitten", "sitting"))
|
|
35
|
+
|
|
36
|
+
parser = Parser([("S", ["NP", "VP"]), ("NP", ["Det", "N"]), ("VP", ["V"])])
|
|
37
|
+
print(parser.top_down_parse(["the", "cat", "chases"]))
|
|
38
|
+
|
|
39
|
+
print(preprocess()["stemmed"][:5])
|
|
40
|
+
|
|
41
|
+
model = NaiveBayes().fit(
|
|
42
|
+
[("fun, couple, love, love", "comedy"), ("fast, furious, shoot", "action")]
|
|
43
|
+
)
|
|
44
|
+
print(model.predict(["love", "fun"]))
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Run every experiment in order: python examples/run_all.py [number ...]."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
import pdacek
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def main(numbers):
|
|
9
|
+
for number in numbers or pdacek.programs():
|
|
10
|
+
print(f"\n{'=' * 70}\nProgram {number}: {pdacek.programs()[number]}\n{'=' * 70}")
|
|
11
|
+
pdacek.run(number)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
if __name__ == "__main__":
|
|
15
|
+
main([int(arg) for arg in sys.argv[1:]])
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.24"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pdacek"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
requires-python = ">=3.9"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
dependencies = ["nltk>=3.8"]
|
|
12
|
+
|
|
13
|
+
[project.optional-dependencies]
|
|
14
|
+
dev = ["pytest>=8.0", "build>=1.2", "twine>=5.0"]
|
|
15
|
+
|
|
16
|
+
[project.scripts]
|
|
17
|
+
pdacek = "pdacek.cli:main"
|
|
18
|
+
|
|
19
|
+
[tool.hatch.build.targets.wheel]
|
|
20
|
+
packages = ["src/pdacek"]
|
|
21
|
+
|
|
22
|
+
[tool.hatch.build.targets.sdist]
|
|
23
|
+
include = [
|
|
24
|
+
"src/pdacek",
|
|
25
|
+
"tests",
|
|
26
|
+
"examples",
|
|
27
|
+
"tools",
|
|
28
|
+
"README.md",
|
|
29
|
+
"CHANGELOG.md",
|
|
30
|
+
"PUBLISH.md",
|
|
31
|
+
"LICENSE",
|
|
32
|
+
"conftest.py",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[tool.pytest.ini_options]
|
|
36
|
+
testpaths = ["tests"]
|
|
37
|
+
addopts = "-q"
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""pdacek - a plain, notebook-friendly NLP lab toolkit.
|
|
2
|
+
|
|
3
|
+
Five self-contained experiments, each available three ways:
|
|
4
|
+
|
|
5
|
+
* run it inline -> ``pdacek.p3()``
|
|
6
|
+
* read the code -> ``print(pdacek.source(3))``
|
|
7
|
+
* write it to disk -> ``pdacek.save(3)``
|
|
8
|
+
|
|
9
|
+
The functions are also importable individually, e.g.
|
|
10
|
+
``from pdacek import min_edit_distance``.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Dict, List, Optional
|
|
17
|
+
|
|
18
|
+
from .programs import PROGRAMS, numbers, run, source, titles
|
|
19
|
+
from .programs.pl01_preprocessing import (
|
|
20
|
+
filtration,
|
|
21
|
+
preprocess,
|
|
22
|
+
stopword_removal,
|
|
23
|
+
stemming,
|
|
24
|
+
tokenization,
|
|
25
|
+
validation,
|
|
26
|
+
)
|
|
27
|
+
from .programs.pl02_ngrams import build_corpus, ngram_counts, ngram_probabilities
|
|
28
|
+
from .programs.pl03_edit_distance import edit_table, med, min_edit_distance, trace_operations
|
|
29
|
+
from .programs.pl04_parsing import Parser
|
|
30
|
+
from .programs.pl05_naive_bayes import NaiveBayes
|
|
31
|
+
|
|
32
|
+
__version__ = "0.1.0"
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"__version__",
|
|
36
|
+
"programs",
|
|
37
|
+
"run",
|
|
38
|
+
"source",
|
|
39
|
+
"save",
|
|
40
|
+
"p1",
|
|
41
|
+
"p2",
|
|
42
|
+
"p3",
|
|
43
|
+
"p4",
|
|
44
|
+
"p5",
|
|
45
|
+
# helpers
|
|
46
|
+
"tokenization",
|
|
47
|
+
"filtration",
|
|
48
|
+
"validation",
|
|
49
|
+
"stopword_removal",
|
|
50
|
+
"stemming",
|
|
51
|
+
"preprocess",
|
|
52
|
+
"build_corpus",
|
|
53
|
+
"ngram_counts",
|
|
54
|
+
"ngram_probabilities",
|
|
55
|
+
"edit_table",
|
|
56
|
+
"med",
|
|
57
|
+
"min_edit_distance",
|
|
58
|
+
"trace_operations",
|
|
59
|
+
"Parser",
|
|
60
|
+
"NaiveBayes",
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def programs() -> Dict[int, str]:
|
|
65
|
+
"""Return ``{number: title}`` for every bundled experiment."""
|
|
66
|
+
return titles()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def save(number: int, directory: "str | Path" = ".") -> Path:
|
|
70
|
+
"""Write a program listing to ``directory`` and return its path."""
|
|
71
|
+
program = PROGRAMS[int(number)]
|
|
72
|
+
target = Path(directory) / program.script_name
|
|
73
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
74
|
+
target.write_text(source(number), encoding="utf-8")
|
|
75
|
+
return target
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _run_all() -> List[int]:
|
|
79
|
+
for number in numbers():
|
|
80
|
+
run(number)
|
|
81
|
+
print()
|
|
82
|
+
return numbers()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def p1() -> None:
|
|
86
|
+
"""Program 1 - text preprocessing pipeline."""
|
|
87
|
+
run(1)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def p2() -> None:
|
|
91
|
+
"""Program 2 - unigram, bigram, trigram probabilities."""
|
|
92
|
+
run(2)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def p3() -> None:
|
|
96
|
+
"""Program 3 - minimum edit distance."""
|
|
97
|
+
run(3)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def p4() -> None:
|
|
101
|
+
"""Program 4 - top-down and bottom-up parsing."""
|
|
102
|
+
run(4)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def p5() -> None:
|
|
106
|
+
"""Program 5 - add-one smoothed Naive Bayes."""
|
|
107
|
+
run(5)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def __getattr__(name: str) -> Optional[object]: # pragma: no cover - tiny shim
|
|
111
|
+
"""Allow ``pdacek.p6`` style access to fail with a clear message."""
|
|
112
|
+
if name.startswith("p") and name[1:].isdigit():
|
|
113
|
+
raise AttributeError(
|
|
114
|
+
f"no program {name[1:]} in this release; available: {numbers()}"
|
|
115
|
+
)
|
|
116
|
+
raise AttributeError(f"module 'pdacek' has no attribute {name!r}")
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Command line entry point: ``pdacek 3``, ``pdacek --list``, ..."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
from typing import List, Optional
|
|
8
|
+
|
|
9
|
+
from . import __version__, numbers, programs, run, save, source
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
13
|
+
parser = argparse.ArgumentParser(
|
|
14
|
+
prog="pdacek",
|
|
15
|
+
description="Run the bundled NLP lab experiments without opening an editor.",
|
|
16
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
17
|
+
epilog=(
|
|
18
|
+
"examples:\n"
|
|
19
|
+
" pdacek run every experiment\n"
|
|
20
|
+
" pdacek 3 run experiment 3\n"
|
|
21
|
+
" pdacek p3 same thing\n"
|
|
22
|
+
" pdacek 3 --source print the listing instead of running it\n"
|
|
23
|
+
" pdacek 3 --save write the listing to the current directory\n"
|
|
24
|
+
),
|
|
25
|
+
)
|
|
26
|
+
parser.add_argument(
|
|
27
|
+
"program",
|
|
28
|
+
nargs="?",
|
|
29
|
+
help="experiment number (1-5) or name such as p3",
|
|
30
|
+
)
|
|
31
|
+
parser.add_argument("--list", "-l", action="store_true", help="list experiments and exit")
|
|
32
|
+
parser.add_argument("--source", "-s", action="store_true", help="print the listing")
|
|
33
|
+
parser.add_argument(
|
|
34
|
+
"--save",
|
|
35
|
+
nargs="?",
|
|
36
|
+
const=".",
|
|
37
|
+
metavar="DIR",
|
|
38
|
+
help="write the listing to DIR (default: current directory)",
|
|
39
|
+
)
|
|
40
|
+
parser.add_argument("--version", "-V", action="version", version=f"pdacek {__version__}")
|
|
41
|
+
return parser
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _parse_number(raw: Optional[str]) -> Optional[int]:
|
|
45
|
+
if raw is None:
|
|
46
|
+
return None
|
|
47
|
+
cleaned = raw.strip().lower().lstrip("p").replace("program", "").strip(" _-")
|
|
48
|
+
try:
|
|
49
|
+
return int(cleaned)
|
|
50
|
+
except ValueError:
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def list_programs() -> None:
|
|
55
|
+
print(f"pdacek {__version__} - bundled experiments\n")
|
|
56
|
+
for number, title in programs().items():
|
|
57
|
+
print(f" {number}. {title}")
|
|
58
|
+
print("\nRun one with: pdacek <number>")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
62
|
+
args = build_parser().parse_args(argv)
|
|
63
|
+
|
|
64
|
+
if args.list:
|
|
65
|
+
list_programs()
|
|
66
|
+
return 0
|
|
67
|
+
|
|
68
|
+
number = _parse_number(args.program)
|
|
69
|
+
if args.program is not None and number is None:
|
|
70
|
+
print(f"error: could not read a program number from {args.program!r}", file=sys.stderr)
|
|
71
|
+
print(f"available: {', '.join(str(n) for n in numbers())}", file=sys.stderr)
|
|
72
|
+
return 2
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
if number is None:
|
|
76
|
+
for n in numbers():
|
|
77
|
+
run(n)
|
|
78
|
+
print()
|
|
79
|
+
return 0
|
|
80
|
+
|
|
81
|
+
if args.save is not None:
|
|
82
|
+
path = save(number, args.save)
|
|
83
|
+
print(f"wrote {path}")
|
|
84
|
+
return 0
|
|
85
|
+
|
|
86
|
+
if args.source:
|
|
87
|
+
print(source(number))
|
|
88
|
+
return 0
|
|
89
|
+
|
|
90
|
+
run(number)
|
|
91
|
+
return 0
|
|
92
|
+
except KeyError as exc:
|
|
93
|
+
print(f"error: {exc.args[0]}", file=sys.stderr)
|
|
94
|
+
return 2
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
if __name__ == "__main__": # pragma: no cover
|
|
98
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Shared helpers: tokenisation and NLTK corpus access with safe fallbacks.
|
|
2
|
+
|
|
3
|
+
The lab machines this toolkit targets are often offline, so every helper here
|
|
4
|
+
degrades to a pure-``re`` implementation instead of raising when an NLTK
|
|
5
|
+
resource (``punkt``, ``stopwords``) has not been downloaded yet.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
from typing import List
|
|
12
|
+
|
|
13
|
+
try: # NLTK is a declared dependency, but keep the package importable without it.
|
|
14
|
+
import nltk
|
|
15
|
+
except ImportError: # pragma: no cover - defensive
|
|
16
|
+
nltk = None # type: ignore[assignment]
|
|
17
|
+
|
|
18
|
+
__all__ = ["tokenize", "stopwords_en", "stemmer", "ensure_punkt", "nltk_available"]
|
|
19
|
+
|
|
20
|
+
_TOKEN_RE = re.compile(r"[A-Za-z]+(?:'[A-Za-z]+)?|\d+|[^\sA-Za-z\d]")
|
|
21
|
+
|
|
22
|
+
# Small built-in list, used only when the NLTK corpus is unavailable.
|
|
23
|
+
_FALLBACK_STOPWORDS = frozenset(
|
|
24
|
+
"""
|
|
25
|
+
a about above after again against all am an and any are as at be because been
|
|
26
|
+
before being below between both but by can did do does doing down during each
|
|
27
|
+
few for from further had has have having he her here hers herself him himself
|
|
28
|
+
his how i if in into is it its itself just me more most my myself no nor not
|
|
29
|
+
now of off on once only or other our ours ourselves out over own same she
|
|
30
|
+
should so some such than that the their theirs them themselves then there
|
|
31
|
+
these they this those through to too under until up very was we were what
|
|
32
|
+
when where which while who whom why will with you your yours yourself
|
|
33
|
+
yourselves
|
|
34
|
+
""".split()
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def nltk_available() -> bool:
|
|
39
|
+
"""Return ``True`` when NLTK can be imported."""
|
|
40
|
+
return nltk is not None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _resource_present(resource: str, package: str) -> bool:
|
|
44
|
+
if nltk is None:
|
|
45
|
+
return False
|
|
46
|
+
try:
|
|
47
|
+
nltk.data.find(f"{package}/{resource}")
|
|
48
|
+
return True
|
|
49
|
+
except LookupError:
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def ensure_punkt() -> bool:
|
|
54
|
+
"""Make sure a sentence/word tokenizer model exists; return availability.
|
|
55
|
+
|
|
56
|
+
NLTK 3.9 renamed the tokenizer data to ``punkt_tab``; both names are tried
|
|
57
|
+
and any download failure (offline machine) is swallowed on purpose.
|
|
58
|
+
"""
|
|
59
|
+
if nltk is None:
|
|
60
|
+
return False
|
|
61
|
+
for resource in ("punkt", "punkt_tab"):
|
|
62
|
+
if _resource_present(resource, "tokenizers"):
|
|
63
|
+
return True
|
|
64
|
+
try:
|
|
65
|
+
nltk.download(resource, quiet=True)
|
|
66
|
+
except Exception: # pragma: no cover - network/permission problems
|
|
67
|
+
pass
|
|
68
|
+
if _resource_present(resource, "tokenizers"):
|
|
69
|
+
return True
|
|
70
|
+
return False
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def tokenize(text: str) -> List[str]:
|
|
74
|
+
"""Split ``text`` into word and punctuation tokens.
|
|
75
|
+
|
|
76
|
+
Uses NLTK's ``word_tokenize`` when the model is available, otherwise a
|
|
77
|
+
regex tokenizer that keeps punctuation as its own token, which is what the
|
|
78
|
+
filtration and script-validation steps in Program 1 need.
|
|
79
|
+
"""
|
|
80
|
+
if ensure_punkt():
|
|
81
|
+
try:
|
|
82
|
+
return nltk.word_tokenize(text) # type: ignore[union-attr]
|
|
83
|
+
except LookupError: # pragma: no cover - stale resource index
|
|
84
|
+
pass
|
|
85
|
+
return _TOKEN_RE.findall(text)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def stopwords_en() -> frozenset:
|
|
89
|
+
"""Return the English stop-word set (NLTK corpus when available)."""
|
|
90
|
+
if nltk is not None:
|
|
91
|
+
try:
|
|
92
|
+
return frozenset(nltk.corpus.stopwords.words("english")) # type: ignore[union-attr]
|
|
93
|
+
except LookupError:
|
|
94
|
+
try:
|
|
95
|
+
nltk.download("stopwords", quiet=True) # type: ignore[union-attr]
|
|
96
|
+
return frozenset(nltk.corpus.stopwords.words("english")) # type: ignore[union-attr]
|
|
97
|
+
except Exception:
|
|
98
|
+
pass
|
|
99
|
+
return _FALLBACK_STOPWORDS
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class _SimpleStemmer:
|
|
103
|
+
"""Very small suffix stripper used when NLTK is not importable."""
|
|
104
|
+
|
|
105
|
+
_SUFFIXES = (
|
|
106
|
+
"ational", "ization", "iveness", "ingly", "edly", "ments",
|
|
107
|
+
"ing", "ies", "ied", "ed", "es", "s",
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
def stem(self, word: str) -> str:
|
|
111
|
+
low = word.lower()
|
|
112
|
+
for suffix in self._SUFFIXES:
|
|
113
|
+
if len(low) > len(suffix) + 2 and low.endswith(suffix):
|
|
114
|
+
if suffix == "ies":
|
|
115
|
+
return low[: -len(suffix)] + "y"
|
|
116
|
+
return low[: -len(suffix)]
|
|
117
|
+
return low
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def stemmer():
|
|
121
|
+
"""Return a Porter stemmer (NLTK) or a minimal fallback stemmer."""
|
|
122
|
+
if nltk is not None:
|
|
123
|
+
try:
|
|
124
|
+
from nltk.stem import PorterStemmer
|
|
125
|
+
|
|
126
|
+
return PorterStemmer()
|
|
127
|
+
except Exception: # pragma: no cover
|
|
128
|
+
pass
|
|
129
|
+
return _SimpleStemmer()
|