Skip to content

Commit 6ee658e

Browse files
committed
feat(compat): implement --compat flag and reorganize tests
Compat Mode: - Add --compat flag to CLI for reserved future Python zipapp strict compatibility mode - Currently accepted and parsed but no behavior changes (placeholder for future use) - Flag is reserved to ensure future compatibility changes don't break existing builds - Users should use --compat when they require stdlib zipapp compatibility Info Command Enhancements: - Add directory auto-derivation for --info command - When SOURCE is a directory, automatically looks for {dirname}/{dirname}.pyz - Improves usability for common pattern where source and built archive share parent directory name Documentation: - Add "Special Modes" section to docs/cli-reference.md - Document --info command with auto-derivation examples - Document --compat flag with clear status and recommendation - Include usage examples for both features - Update ROADMAP.md to track --compat flag as implemented Test Reorganization: - Move 18 CLI integration tests from tests/50_core/ to tests/90_integration/ - Create new tests/95_integration_output/ for subprocess-based end-to-end tests - Add comprehensive zipapp compatibility integration test suite (15 tests) - Test all main CLI features with --compat flag for future compatibility Code Quality: - Extract _display_metadata() helper in info.py to reduce complexity (14 → 9 branches) - Remove 4 placeholder CLI argument references (preset, list_presets, tree, count) - All lint and type checking errors fixed Tests: 299 passed, 1 skipped
1 parent 9828479 commit 6ee658e

23 files changed

+505
-36
lines changed

ROADMAP.md

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,30 +3,21 @@
33

44
**Important Clarification**: Zipbundler provides Bundle your packages into a runnable, importable zip
55

6-
## Key Points
7-
8-
Some of these we just want to consider, and may not want to implement.
9-
106
## 🎯 Core Features
7+
- None at this time.
118

129
## 🧰 CLI Commands
1310

1411
### CLI Arguments (Implemented)
1512

16-
#### Build Flags
17-
- **`--input`** / `--in`: Override the name of the input file or directory. Start from an existing build (usually optional). ✅ Implemented
18-
1913
#### Universal Flags
2014
- **`--compat`** / `--compatability`** / `compat`: Compatibility mode with stdlib zipapp behaviour. Currently defined but not implemented.
2115

22-
### CLI Commands (Other)
23-
- audit for missing CLI arguments
24-
- watch argument as float
25-
2616
## ⚙️ Configuration Features
27-
17+
- None at this time.
2818

2919
## 🧪 Testing
20+
- None at this time.
3021

3122
## 🧑‍💻 Development
3223

@@ -54,19 +45,21 @@ The following constants are defined in `constants.py` but not yet used in the co
5445
- **BUILD_TIMESTAMP_PLACEHOLDER**: Placeholder string for build timestamps (default: "<build-timestamp>"). Used for deterministic builds
5546

5647
## 🔌 API Implementation
57-
48+
- None at this time.
5849

5950
## 📚 Documentation
51+
- None at this time.
6052

6153
## 🚀 Deployment
54+
- None at this time.
6255

6356
## 💡 Future Ideas
6457

6558
- **Multi-format Support**: Support other archive formats
6659
- **Plugin System**: Extensible architecture for custom handlers
6760

6861
## 🔧 Development Infrastructure
69-
62+
- None at this time.
7063

7164
> See [REJECTED.md](REJECTED.md) for experiments and ideas that were explored but intentionally not pursued.
7265

docs/cli-reference.md

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ zipbundler SOURCE [OPTIONS]
2323
- `-p, --python PYTHON`: Python interpreter path for shebang line (default: no shebang)
2424
- `-m, --main MAIN`: Main entry point as `module:function` or `module` (default: use existing `__main__.py`)
2525
- `-c, --compress`: Compress files with deflate method (default: uncompressed)
26-
- `--info`: Display the interpreter from an existing archive
26+
- `--info`: Display the interpreter and metadata from an existing archive (see `--info` section below)
27+
- `--compat`: Enable compatibility mode (reserved for future strict Python zipapp compatibility; currently no behavior changes)
2728

2829
**Examples:**
2930
```bash
@@ -42,8 +43,14 @@ zipbundler src/myapp -o app.pyz -m "myapp:main" --compress
4243
# Display info from existing archive
4344
zipbundler app.pyz --info
4445

46+
# Display info from archive using directory auto-derivation
47+
zipbundler myapp --info
48+
4549
# Modify existing archive (requires -o)
4650
zipbundler app.pyz -o app_new.pyz -p "/usr/bin/env python3"
51+
52+
# Build with zipapp compatibility mode
53+
zipbundler src/myapp -o app.pyz --compat
4754
```
4855

4956
**Compatibility Notes:**
@@ -53,6 +60,55 @@ zipbundler app.pyz -o app_new.pyz -p "/usr/bin/env python3"
5360
-**Flat structure** — Preserves original package paths without transformations
5461
-**Archive reading** — Supports reading and modifying existing `.pyz` files
5562

63+
## Special Modes
64+
65+
### `--info` Command
66+
67+
Display interpreter and metadata information from an existing archive.
68+
69+
```bash
70+
zipbundler --info ARCHIVE
71+
zipbundler --info DIRECTORY
72+
```
73+
74+
The `--info` command shows:
75+
- Interpreter shebang from the archive (if present)
76+
- Metadata: name, version, description, author, license
77+
78+
**Directory Auto-Derivation:**
79+
If SOURCE is a directory, zipbundler automatically looks for `{dirname}/{dirname}.pyz` in that directory. This is convenient for the common pattern where your source and built archive share the same parent directory name.
80+
81+
**Examples:**
82+
```bash
83+
# Display info from a specific archive file
84+
zipbundler --info dist/myapp.pyz
85+
86+
# Display info using directory auto-derivation
87+
# Looks for: myapp/myapp.pyz
88+
zipbundler --info myapp
89+
```
90+
91+
### `--compat` Flag
92+
93+
Enable compatibility mode for Python zipapp. This flag is **reserved for future strict Python zipapp compatibility** and ensures your builds will remain compatible with stdlib `zipapp` behavior in future versions.
94+
95+
**Current Status:**
96+
- ✅ Flag is accepted and parsed
97+
- ℹ️ Currently no behavior changes when enabled
98+
- 🔮 May implement strict compatibility mode in future versions
99+
100+
**Recommendation:**
101+
Use `--compat` if you require compatibility with Python's stdlib `zipapp` module. This ensures your builds won't break if we add strict compatibility mode features in the future.
102+
103+
**Example:**
104+
```bash
105+
# Build with compatibility mode enabled
106+
zipbundler src/myapp -o app.pyz --compat
107+
108+
# Works with all other options
109+
zipbundler src/myapp -o app.pyz -p "/usr/bin/env python3" -m "myapp:main" --compat
110+
```
111+
56112
## Commands
57113

58114
### `zipbundler build`

src/zipbundler/cli.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -528,10 +528,6 @@ def _prepare_init_args(parsed_args: argparse.Namespace) -> argparse.Namespace:
528528
# Use --config to specify where to create the config file
529529
init_args.config = getattr(parsed_args, "config", None)
530530
init_args.force = getattr(parsed_args, "force", False)
531-
# Verify this arg exists
532-
init_args.preset = getattr(parsed_args, "preset", None)
533-
# Verify this arg exists
534-
init_args.list_presets = getattr(parsed_args, "list_presets", False)
535531
init_args.log_level = parsed_args.log_level
536532
return init_args
537533

@@ -578,8 +574,6 @@ def _prepare_list_args(parsed_args: argparse.Namespace) -> argparse.Namespace:
578574
list_args = argparse.Namespace()
579575
# Include is now always a list from nargs="*"
580576
list_args.include = parsed_args.include if parsed_args.include else []
581-
list_args.tree = getattr(parsed_args, "tree", None) # Verify this arg exists
582-
list_args.count = getattr(parsed_args, "count", None) # Verify this arg exists
583577
list_args.log_level = parsed_args.log_level
584578
return list_args
585579

src/zipbundler/commands/info.py

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,39 @@
33
"""Handle the --info flag for displaying interpreter and metadata from archive."""
44

55
import argparse
6+
from pathlib import Path
67

78
from zipbundler.build import get_interpreter, get_metadata_from_archive
89
from zipbundler.logs import getAppLogger
910

1011

12+
def _display_metadata(logger: object, metadata: dict[str, str]) -> None:
13+
"""Display metadata from archive."""
14+
logger.info("") # type: ignore[union-attr]
15+
logger.info("Metadata:") # type: ignore[union-attr]
16+
fields = [
17+
("display_name", "Name"),
18+
("version", "Version"),
19+
("description", "Description"),
20+
("author", "Author"),
21+
("license", "License"),
22+
]
23+
for key, label in fields:
24+
if key in metadata:
25+
logger.info(" %s: %s", label, metadata[key]) # type: ignore[union-attr]
26+
27+
1128
def handle_info_command(
1229
source: str | None,
1330
parser: argparse.ArgumentParser,
1431
) -> int:
1532
"""Handle the --info flag for displaying interpreter and metadata from archive.
1633
34+
If source is a directory, auto-derives the output filename (source.stem + .pyz)
35+
and looks for the built archive in that location.
36+
1737
Args:
18-
source: Path to the archive file
38+
source: Path to the archive file or directory
1939
parser: Argument parser for error handling
2040
2141
Returns:
@@ -27,29 +47,28 @@ def handle_info_command(
2747
parser.error("--info requires SOURCE archive path")
2848
return 1 # pragma: no cover
2949

50+
source_path = Path(source).resolve()
51+
52+
# If source is a directory, derive the output filename
53+
if source_path.is_dir():
54+
# Auto-derive output name: directory name + .pyz
55+
archive_path = source_path / f"{source_path.name}.pyz"
56+
logger.debug("Source is a directory, looking for archive: %s", archive_path)
57+
else:
58+
archive_path = source_path
59+
3060
try:
3161
# Display interpreter
32-
interpreter = get_interpreter(source)
62+
interpreter = get_interpreter(archive_path)
3363
if interpreter is None:
3464
logger.info("No interpreter specified in archive")
3565
else:
3666
logger.info("Interpreter: %s", interpreter)
3767

3868
# Display metadata if present
39-
metadata = get_metadata_from_archive(source)
69+
metadata = get_metadata_from_archive(archive_path)
4070
if metadata:
41-
logger.info("")
42-
logger.info("Metadata:")
43-
if "display_name" in metadata:
44-
logger.info(" Name: %s", metadata["display_name"])
45-
if "version" in metadata:
46-
logger.info(" Version: %s", metadata["version"])
47-
if "description" in metadata:
48-
logger.info(" Description: %s", metadata["description"])
49-
if "author" in metadata:
50-
logger.info(" Author: %s", metadata["author"])
51-
if "license" in metadata:
52-
logger.info(" License: %s", metadata["license"])
71+
_display_metadata(logger, metadata)
5372
except (FileNotFoundError, ValueError):
5473
logger.exception("Failed to get info from archive")
5574
return 1
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.

0 commit comments

Comments
 (0)