mirror of
https://github.com/TECHNOFAB11/bumpver.git
synced 2025-12-12 14:30:09 +01:00
module reorg
This commit is contained in:
parent
e1aaf7629b
commit
8af5047244
23 changed files with 1658 additions and 1532 deletions
|
|
@ -8,7 +8,6 @@
|
||||||
- Better support for optional parts.
|
- Better support for optional parts.
|
||||||
- New: Start `BUILD` parts at `1000` to avoid leading zero truncation.
|
- New: Start `BUILD` parts at `1000` to avoid leading zero truncation.
|
||||||
- New gitlab #10: `--pin-date` to keep date parts unchanged, and only increment non-date parts.
|
- New gitlab #10: `--pin-date` to keep date parts unchanged, and only increment non-date parts.
|
||||||
- New: enable globs for filenames in `pycalver:file_patterns`
|
|
||||||
- Fix gitlab #8: Push tags only pushed tags, not actual commit.
|
- Fix gitlab #8: Push tags only pushed tags, not actual commit.
|
||||||
- Fix gitlab #9: Make commit message configurable.
|
- Fix gitlab #9: Make commit message configurable.
|
||||||
|
|
||||||
|
|
|
||||||
136
pylint-ignore.md
136
pylint-ignore.md
|
|
@ -23,7 +23,8 @@ The recommended approach to using `pylint-ignore` is:
|
||||||
|
|
||||||
# Overview
|
# Overview
|
||||||
|
|
||||||
- [W0511: fixme (7x)](#w0511-fixme)
|
- [E1123: unexpected-keyword-arg (1x)](#e1123-unexpected-keyword-arg)
|
||||||
|
- [W0511: fixme (9x)](#w0511-fixme)
|
||||||
- [W0703: broad-except (1x)](#w0703-broad-except)
|
- [W0703: broad-except (1x)](#w0703-broad-except)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -36,7 +37,7 @@ The recommended approach to using `pylint-ignore` is:
|
||||||
- `date : 2020-09-18T17:01:05`
|
- `date : 2020-09-18T17:01:05`
|
||||||
|
|
||||||
```
|
```
|
||||||
12: import pycalver2.patterns as v2patterns
|
12: from pycalver import v2patterns
|
||||||
13:
|
13:
|
||||||
> 14: # TODO (mb 2020-09-06): test for v2patterns
|
> 14: # TODO (mb 2020-09-06): test for v2patterns
|
||||||
15:
|
15:
|
||||||
|
|
@ -61,107 +62,138 @@ The recommended approach to using `pylint-ignore` is:
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
## File test/test_version.py - Line 167 - W0511 (fixme)
|
## File test/test_config.py - Line 156 - W0511 (fixme)
|
||||||
|
|
||||||
|
- `message: TODO (mb 2020-09-18):`
|
||||||
|
- `author : Manuel Barkhau <mbarkhau@gmail.com>`
|
||||||
|
- `date : 2020-09-18T19:04:06`
|
||||||
|
|
||||||
|
```
|
||||||
|
143: def test_parse_v2_cfg():
|
||||||
|
...
|
||||||
|
154: assert "setup.py" in cfg.file_patterns
|
||||||
|
155: assert "setup.cfg" in cfg.file_patterns
|
||||||
|
> 156: # TODO (mb 2020-09-18):
|
||||||
|
157: # assert cfg.file_patterns["setup.py" ] == ["vYYYY0M.BUILD[-TAG]", "YYYY0M.BLD[PYTAGNUM]"]
|
||||||
|
158: # assert cfg.file_patterns["setup.cfg" ] == ['current_version = "vYYYY0M.BUILD[-TAG]"']
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## File test/test_version.py - Line 168 - W0511 (fixme)
|
||||||
|
|
||||||
- `message: TODO (mb 2020-09-06): add tests for new style patterns`
|
- `message: TODO (mb 2020-09-06): add tests for new style patterns`
|
||||||
- `author : Manuel Barkhau <mbarkhau@gmail.com>`
|
- `author : Manuel Barkhau <mbarkhau@gmail.com>`
|
||||||
- `date : 2020-09-18T17:01:05`
|
- `date : 2020-09-18T17:01:05`
|
||||||
|
|
||||||
```
|
```
|
||||||
162: def vnfo(**field_values):
|
163: def vnfo(**field_values):
|
||||||
...
|
...
|
||||||
165:
|
166:
|
||||||
166: PARSE_VERSION_TEST_CASES = [
|
167: PARSE_VERSION_TEST_CASES = [
|
||||||
> 167: # TODO (mb 2020-09-06): add tests for new style patterns
|
> 168: # TODO (mb 2020-09-06): add tests for new style patterns
|
||||||
168: # ["YYYY.MM.DD" , "2017.06.07", vnfo(year="2017", month="06", dom="07")],
|
169: # ["YYYY.MM.DD" , "2017.06.07", vnfo(year="2017", month="06", dom="07")],
|
||||||
169: ["{year}.{month}.{dom}" , "2017.06.07", vnfo(year="2017", month="06", dom="07")],
|
170: ["{year}.{month}.{dom}" , "2017.06.07", vnfo(year="2017", month="06", dom="07")],
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
## File src/pycalver/__main__.py - Line 229 - W0511 (fixme)
|
## File src/pycalver/v1patterns.py - Line 212 - W0511 (fixme)
|
||||||
|
|
||||||
- `message: TODO (mb 2020-09-05): version switch`
|
- `message: TODO (mb 2020-09-19): replace {version} etc with version_pattern`
|
||||||
- `author : Manuel Barkhau <mbarkhau@gmail.com>`
|
- `author : Manuel Barkhau <mbarkhau@gmail.com>`
|
||||||
- `date : 2020-09-05T14:30:17`
|
- `date : 2020-09-19T16:24:10`
|
||||||
|
|
||||||
```
|
```
|
||||||
209: def _bump(
|
199: def _compile_pattern_re(version_pattern: str, raw_pattern: str) -> typ.Pattern[str]:
|
||||||
...
|
...
|
||||||
227:
|
210: escaped_pattern = escaped_pattern.replace(char, escaped)
|
||||||
228: try:
|
211:
|
||||||
> 229: # TODO (mb 2020-09-05): version switch
|
> 212: # TODO (mb 2020-09-19): replace {version} etc with version_pattern
|
||||||
230: v1cli.rewrite(cfg, new_version)
|
213: pattern_str = _replace_pattern_parts(escaped_pattern)
|
||||||
231: # v2cli.rewrite(cfg, new_version)
|
214: return re.compile(pattern_str)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
## File src/pycalver/config.py - Line 236 - W0511 (fixme)
|
## File src/pycalver/__main__.py - Line 247 - W0511 (fixme)
|
||||||
|
|
||||||
- `message: TODO (mb 2020-09-06): new style pattern by default`
|
- `message: TODO (mb 2020-09-18): Investigate error messages`
|
||||||
- `author : Manuel Barkhau <mbarkhau@gmail.com>`
|
- `author : Manuel Barkhau <mbarkhau@gmail.com>`
|
||||||
- `date : 2020-09-18T17:01:05`
|
- `date : 2020-09-19T16:24:10`
|
||||||
|
|
||||||
```
|
```
|
||||||
227: def _parse_config(raw_cfg: RawConfig) -> Config:
|
219: def _bump(
|
||||||
...
|
...
|
||||||
234: version_str = raw_cfg['current_version'] = version_str.strip("'\" ")
|
245: sys.exit(1)
|
||||||
235:
|
246: except Exception as ex:
|
||||||
> 236: # TODO (mb 2020-09-06): new style pattern by default
|
> 247: # TODO (mb 2020-09-18): Investigate error messages
|
||||||
237: # version_pattern: str = raw_cfg.get('version_pattern', "vYYYY0M.BUILD[-TAG]")
|
248: logger.error(str(ex))
|
||||||
238: version_pattern: str = raw_cfg.get('version_pattern', "{pycalver}")
|
249: sys.exit(1)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
## File src/pycalver/__main__.py - Line 285 - W0511 (fixme)
|
## File src/pycalver/v2patterns.py - Line 256 - W0511 (fixme)
|
||||||
|
|
||||||
- `message: TODO (mb 2020-09-05): version switch`
|
- `message: TODO (mb 2020-09-19): replace {version} etc with version_pattern`
|
||||||
- `author : Manuel Barkhau <mbarkhau@gmail.com>`
|
- `author : Manuel Barkhau <mbarkhau@gmail.com>`
|
||||||
- `date : 2020-09-05T14:30:17`
|
- `date : 2020-09-19T16:24:10`
|
||||||
|
|
||||||
```
|
```
|
||||||
282: def _update_cfg_from_vcs(cfg: config.Config, fetch: bool) -> config.Config:
|
240: def _compile_pattern_re(version_pattern: str, raw_pattern: str) -> typ.Pattern[str]:
|
||||||
...
|
...
|
||||||
283: all_tags = vcs.get_tags(fetch=fetch)
|
254: print("<<<<", (normalized_pattern,))
|
||||||
284:
|
255:
|
||||||
> 285: # TODO (mb 2020-09-05): version switch
|
> 256: # TODO (mb 2020-09-19): replace {version} etc with version_pattern
|
||||||
286: cfg = v1cli.update_cfg_from_vcs(cfg, all_tags)
|
257: pattern_str = _replace_pattern_parts(escaped_pattern)
|
||||||
287: # cfg = v2cli.update_cfg_from_vcs(cfg, all_tags)
|
258: return re.compile(pattern_str)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
## File src/pycalver/__main__.py - Line 392 - W0511 (fixme)
|
## File src/pycalver/config.py - Line 264 - W0511 (fixme)
|
||||||
|
|
||||||
- `message: # TODO (mb 2020-09-05): format from config`
|
- `message: TODO (mb 2020-09-18): Validate Pattern`
|
||||||
- `author : Manuel Barkhau <mbarkhau@gmail.com>`
|
- `author : Manuel Barkhau <mbarkhau@gmail.com>`
|
||||||
- `date : 2020-09-05T14:30:17`
|
- `date : 2020-09-18T19:04:06`
|
||||||
|
|
||||||
```
|
```
|
||||||
336: def bump(
|
250: def _parse_config(raw_cfg: RawConfig) -> Config:
|
||||||
...
|
...
|
||||||
390: return
|
262: is_new_pattern = "{" not in version_pattern and "}" not in version_pattern
|
||||||
391:
|
263:
|
||||||
> 392: # # TODO (mb 2020-09-05): format from config
|
> 264: # TODO (mb 2020-09-18): Validate Pattern
|
||||||
393: # commit_message_kwargs = {
|
265: # detect YY with WW or UU -> suggest GG with VV
|
||||||
394: # new_version
|
266: # detect YYMM -> suggest YY0M
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## File test/test_cli.py - Line 536 - W0511 (fixme)
|
||||||
|
|
||||||
|
- `message: # TODO (mb 2020-09-18):`
|
||||||
|
- `author : Manuel Barkhau <mbarkhau@gmail.com>`
|
||||||
|
- `date : 2020-09-18T19:35:32`
|
||||||
|
|
||||||
|
```
|
||||||
|
534:
|
||||||
|
535: # def test_custom_commit_message(runner):
|
||||||
|
> 536: # # TODO (mb 2020-09-18):
|
||||||
|
537: # assert False
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
# W0703: broad-except
|
# W0703: broad-except
|
||||||
|
|
||||||
## File src/pycalver/__main__.py - Line 232 - W0703 (broad-except)
|
## File src/pycalver/__main__.py - Line 246 - W0703 (broad-except)
|
||||||
|
|
||||||
- `message: Catching too general exception Exception`
|
- `message: Catching too general exception Exception`
|
||||||
- `author : Manuel Barkhau <mbarkhau@gmail.com>`
|
- `author : Manuel Barkhau <mbarkhau@gmail.com>`
|
||||||
- `date : 2020-09-05T14:30:17`
|
- `date : 2020-09-05T14:30:17`
|
||||||
|
|
||||||
```
|
```
|
||||||
209: def _bump(
|
219: def _bump(
|
||||||
...
|
...
|
||||||
230: v1cli.rewrite(cfg, new_version)
|
244: logger.error(str(ex))
|
||||||
231: # v2cli.rewrite(cfg, new_version)
|
245: sys.exit(1)
|
||||||
> 232: except Exception as ex:
|
> 246: except Exception as ex:
|
||||||
233: logger.error(str(ex))
|
247: # TODO (mb 2020-09-18): Investigate error messages
|
||||||
234: sys.exit(1)
|
248: logger.error(str(ex))
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,14 +16,14 @@ import subprocess as sp
|
||||||
|
|
||||||
import click
|
import click
|
||||||
|
|
||||||
import pycalver.cli as v1cli
|
from . import vcs
|
||||||
import pycalver2.cli as v2cli
|
from . import v1cli
|
||||||
import pycalver.version as v1version
|
from . import v2cli
|
||||||
import pycalver2.version as v2version
|
from . import config
|
||||||
import pycalver.rewrite as v1rewrite
|
from . import rewrite
|
||||||
|
from . import version
|
||||||
from pycalver import vcs
|
from . import v1version
|
||||||
from pycalver import config
|
from . import v2version
|
||||||
|
|
||||||
_VERBOSE = 0
|
_VERBOSE = 0
|
||||||
|
|
||||||
|
|
@ -110,7 +110,7 @@ def test(
|
||||||
|
|
||||||
new_version = _incr(
|
new_version = _incr(
|
||||||
old_version,
|
old_version,
|
||||||
pattern=pattern,
|
raw_pattern=pattern,
|
||||||
release=release,
|
release=release,
|
||||||
major=major,
|
major=major,
|
||||||
minor=minor,
|
minor=minor,
|
||||||
|
|
@ -121,9 +121,7 @@ def test(
|
||||||
logger.error(f"Invalid version '{old_version}' and/or pattern '{pattern}'.")
|
logger.error(f"Invalid version '{old_version}' and/or pattern '{pattern}'.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# TODO (mb 2020-09-05): version switch
|
pep440_version = version.to_pep440(new_version)
|
||||||
pep440_version = v1version.to_pep440(new_version)
|
|
||||||
# pep440_version = v2version.to_pep440(new_version)
|
|
||||||
|
|
||||||
click.echo(f"New Version: {new_version}")
|
click.echo(f"New Version: {new_version}")
|
||||||
click.echo(f"PEP440 : {pep440_version}")
|
click.echo(f"PEP440 : {pep440_version}")
|
||||||
|
|
@ -150,7 +148,7 @@ def show(verbose: int = 0, fetch: bool = True) -> None:
|
||||||
click.echo(f"PEP440 : {cfg.pep440_version}")
|
click.echo(f"PEP440 : {cfg.pep440_version}")
|
||||||
|
|
||||||
|
|
||||||
def _print_diff(diff: str) -> None:
|
def _print_diff_str(diff: str) -> None:
|
||||||
if sys.stdout.isatty():
|
if sys.stdout.isatty():
|
||||||
for line in diff.splitlines():
|
for line in diff.splitlines():
|
||||||
if line.startswith("+++") or line.startswith("---"):
|
if line.startswith("+++") or line.startswith("---"):
|
||||||
|
|
@ -167,9 +165,27 @@ def _print_diff(diff: str) -> None:
|
||||||
click.echo(diff)
|
click.echo(diff)
|
||||||
|
|
||||||
|
|
||||||
|
def _print_diff(cfg: config.Config, new_version: str) -> None:
|
||||||
|
try:
|
||||||
|
if cfg.is_new_pattern:
|
||||||
|
diff = v2cli.get_diff(cfg, new_version)
|
||||||
|
else:
|
||||||
|
diff = v1cli.get_diff(cfg, new_version)
|
||||||
|
|
||||||
|
_print_diff_str(diff)
|
||||||
|
except rewrite.NoPatternMatch as ex:
|
||||||
|
logger.error(str(ex))
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as ex:
|
||||||
|
# pylint:disable=broad-except; Mostly we expect IOError here, but
|
||||||
|
# could be other things and there's no option to recover anyway.
|
||||||
|
logger.error(str(ex))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def _incr(
|
def _incr(
|
||||||
old_version: str,
|
old_version: str,
|
||||||
pattern : str = "{pycalver}",
|
raw_pattern: str = "{pycalver}",
|
||||||
*,
|
*,
|
||||||
release : str = None,
|
release : str = None,
|
||||||
major : bool = False,
|
major : bool = False,
|
||||||
|
|
@ -177,11 +193,11 @@ def _incr(
|
||||||
patch : bool = False,
|
patch : bool = False,
|
||||||
pin_date: bool = False,
|
pin_date: bool = False,
|
||||||
) -> typ.Optional[str]:
|
) -> typ.Optional[str]:
|
||||||
is_v1_pattern = "{" in pattern
|
is_new_pattern = "{" in raw_pattern and "}" in raw_pattern
|
||||||
if is_v1_pattern:
|
if is_new_pattern:
|
||||||
return v1version.incr(
|
return v2version.incr(
|
||||||
old_version,
|
old_version,
|
||||||
pattern=pattern,
|
raw_pattern=raw_pattern,
|
||||||
release=release,
|
release=release,
|
||||||
major=major,
|
major=major,
|
||||||
minor=minor,
|
minor=minor,
|
||||||
|
|
@ -189,9 +205,9 @@ def _incr(
|
||||||
pin_date=pin_date,
|
pin_date=pin_date,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return v2version.incr(
|
return v1version.incr(
|
||||||
old_version,
|
old_version,
|
||||||
pattern=pattern,
|
raw_pattern=raw_pattern,
|
||||||
release=release,
|
release=release,
|
||||||
major=major,
|
major=major,
|
||||||
minor=minor,
|
minor=minor,
|
||||||
|
|
@ -221,10 +237,10 @@ def _bump(
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if cfg.is_new_pattern:
|
if cfg.is_new_pattern:
|
||||||
v2cli.rewrite(cfg, new_version)
|
v2cli.rewrite_files(cfg, new_version)
|
||||||
else:
|
else:
|
||||||
v1cli.rewrite(cfg, new_version)
|
v1cli.rewrite_files(cfg, new_version)
|
||||||
except v1rewrite.NoPatternMatch as ex:
|
except rewrite.NoPatternMatch as ex:
|
||||||
logger.error(str(ex))
|
logger.error(str(ex))
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
|
|
@ -266,11 +282,11 @@ def init(verbose: int = 0, dry: bool = False) -> None:
|
||||||
cfg: config.MaybeConfig = config.parse(ctx)
|
cfg: config.MaybeConfig = config.parse(ctx)
|
||||||
|
|
||||||
if cfg:
|
if cfg:
|
||||||
logger.error(f"Configuration already initialized in {ctx.config_filepath}")
|
logger.error(f"Configuration already initialized in {ctx.config_rel_path}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
if dry:
|
if dry:
|
||||||
click.echo(f"Exiting because of '--dry'. Would have written to {ctx.config_filepath}:")
|
click.echo(f"Exiting because of '--dry'. Would have written to {ctx.config_rel_path}:")
|
||||||
cfg_text: str = config.default_config(ctx)
|
cfg_text: str = config.default_config(ctx)
|
||||||
click.echo("\n " + "\n ".join(cfg_text.splitlines()))
|
click.echo("\n " + "\n ".join(cfg_text.splitlines()))
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
@ -362,7 +378,7 @@ def bump(
|
||||||
old_version = cfg.current_version
|
old_version = cfg.current_version
|
||||||
new_version = _incr(
|
new_version = _incr(
|
||||||
old_version,
|
old_version,
|
||||||
pattern=cfg.version_pattern,
|
raw_pattern=cfg.version_pattern,
|
||||||
release=release,
|
release=release,
|
||||||
major=major,
|
major=major,
|
||||||
minor=minor,
|
minor=minor,
|
||||||
|
|
@ -387,20 +403,7 @@ def bump(
|
||||||
logger.info(f"New Version: {new_version}")
|
logger.info(f"New Version: {new_version}")
|
||||||
|
|
||||||
if dry or verbose >= 2:
|
if dry or verbose >= 2:
|
||||||
try:
|
_print_diff(cfg, new_version)
|
||||||
if cfg.is_new_pattern:
|
|
||||||
diff = v2cli.get_diff(cfg, new_version)
|
|
||||||
else:
|
|
||||||
diff = v1cli.get_diff(cfg, new_version)
|
|
||||||
_print_diff(diff)
|
|
||||||
except v1rewrite.NoPatternMatch as ex:
|
|
||||||
logger.error(str(ex))
|
|
||||||
sys.exit(1)
|
|
||||||
except Exception as ex:
|
|
||||||
# pylint:disable=broad-except; Mostly we expect IOError here, but
|
|
||||||
# could be other things and there's no option to recover anyway.
|
|
||||||
logger.error(str(ex))
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
if dry:
|
if dry:
|
||||||
return
|
return
|
||||||
|
|
@ -408,8 +411,8 @@ def bump(
|
||||||
commit_message_kwargs = {
|
commit_message_kwargs = {
|
||||||
'new_version' : new_version,
|
'new_version' : new_version,
|
||||||
'old_version' : old_version,
|
'old_version' : old_version,
|
||||||
'new_version_pep440': v1version.to_pep440(new_version),
|
'new_version_pep440': version.to_pep440(new_version),
|
||||||
'old_version_pep440': v1version.to_pep440(old_version),
|
'old_version_pep440': version.to_pep440(old_version),
|
||||||
}
|
}
|
||||||
commit_message = cfg.commit_message.format(**commit_message_kwargs)
|
commit_message = cfg.commit_message.format(**commit_message_kwargs)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,13 +14,22 @@ import configparser
|
||||||
import toml
|
import toml
|
||||||
import pathlib2 as pl
|
import pathlib2 as pl
|
||||||
|
|
||||||
import pycalver.version as v1version
|
from . import version
|
||||||
import pycalver2.version as v2version
|
from . import v1version
|
||||||
|
from . import v2version
|
||||||
|
from . import v1patterns
|
||||||
|
from . import v2patterns
|
||||||
|
from .patterns import Pattern
|
||||||
|
|
||||||
logger = logging.getLogger("pycalver.config")
|
logger = logging.getLogger("pycalver.config")
|
||||||
|
|
||||||
Patterns = typ.List[str]
|
RawPatterns = typ.List[str]
|
||||||
PatternsByGlob = typ.Dict[str, Patterns]
|
RawPatternsByFile = typ.Dict[str, RawPatterns]
|
||||||
|
FileRawPatternsItem = typ.Tuple[str, RawPatterns]
|
||||||
|
|
||||||
|
PatternsByFile = typ.Dict[str, typ.List[Pattern]]
|
||||||
|
FilePatternsItem = typ.Tuple[str, typ.List[Pattern]]
|
||||||
|
|
||||||
|
|
||||||
SUPPORTED_CONFIGS = ["setup.cfg", "pyproject.toml", "pycalver.toml"]
|
SUPPORTED_CONFIGS = ["setup.cfg", "pyproject.toml", "pycalver.toml"]
|
||||||
|
|
||||||
|
|
@ -32,6 +41,7 @@ class ProjectContext(typ.NamedTuple):
|
||||||
|
|
||||||
path : pl.Path
|
path : pl.Path
|
||||||
config_filepath: pl.Path
|
config_filepath: pl.Path
|
||||||
|
config_rel_path: str
|
||||||
config_format : str
|
config_format : str
|
||||||
vcs_type : typ.Optional[str]
|
vcs_type : typ.Optional[str]
|
||||||
|
|
||||||
|
|
@ -60,6 +70,12 @@ def init_project_ctx(project_path: typ.Union[str, pl.Path, None] = ".") -> Proje
|
||||||
config_filepath = path / "pycalver.toml"
|
config_filepath = path / "pycalver.toml"
|
||||||
config_format = 'toml'
|
config_format = 'toml'
|
||||||
|
|
||||||
|
if config_filepath.is_absolute():
|
||||||
|
config_rel_path = str(config_filepath.relative_to(path.absolute()))
|
||||||
|
else:
|
||||||
|
config_rel_path = str(config_filepath)
|
||||||
|
config_filepath = pl.Path.cwd() / config_filepath
|
||||||
|
|
||||||
vcs_type: typ.Optional[str]
|
vcs_type: typ.Optional[str]
|
||||||
|
|
||||||
if (path / ".git").exists():
|
if (path / ".git").exists():
|
||||||
|
|
@ -69,10 +85,11 @@ def init_project_ctx(project_path: typ.Union[str, pl.Path, None] = ".") -> Proje
|
||||||
else:
|
else:
|
||||||
vcs_type = None
|
vcs_type = None
|
||||||
|
|
||||||
return ProjectContext(path, config_filepath, config_format, vcs_type)
|
return ProjectContext(path, config_filepath, config_rel_path, config_format, vcs_type)
|
||||||
|
|
||||||
|
|
||||||
RawConfig = typ.Dict[str, typ.Any]
|
RawConfig = typ.Dict[str, typ.Any]
|
||||||
|
MaybeRawConfig = typ.Optional[RawConfig]
|
||||||
|
|
||||||
|
|
||||||
class Config(typ.NamedTuple):
|
class Config(typ.NamedTuple):
|
||||||
|
|
@ -88,56 +105,46 @@ class Config(typ.NamedTuple):
|
||||||
push : bool
|
push : bool
|
||||||
is_new_pattern: bool
|
is_new_pattern: bool
|
||||||
|
|
||||||
file_patterns: PatternsByGlob
|
file_patterns: PatternsByFile
|
||||||
|
|
||||||
|
|
||||||
|
MaybeConfig = typ.Optional[Config]
|
||||||
|
|
||||||
|
|
||||||
def _debug_str(cfg: Config) -> str:
|
def _debug_str(cfg: Config) -> str:
|
||||||
cfg_str_parts = [
|
cfg_str_parts = [
|
||||||
"Config Parsed: Config(",
|
"Config Parsed: Config(",
|
||||||
f"current_version='{cfg.current_version}'",
|
f"\n current_version='{cfg.current_version}',",
|
||||||
f"version_pattern='{cfg.version_pattern}'",
|
f"\n version_pattern='{cfg.version_pattern}',",
|
||||||
f"pep440_version='{cfg.pep440_version}'",
|
f"\n pep440_version='{cfg.pep440_version}',",
|
||||||
f"commit_message='{cfg.commit_message}'",
|
f"\n commit_message='{cfg.commit_message}',",
|
||||||
f"commit={cfg.commit}",
|
f"\n commit={cfg.commit},",
|
||||||
f"tag={cfg.tag}",
|
f"\n tag={cfg.tag},",
|
||||||
f"push={cfg.push}",
|
f"\n push={cfg.push},",
|
||||||
f"is_new_pattern={cfg.is_new_pattern}",
|
f"\n is_new_pattern={cfg.is_new_pattern},",
|
||||||
"file_patterns={",
|
"\n file_patterns={",
|
||||||
]
|
]
|
||||||
|
|
||||||
for filepath, patterns in cfg.file_patterns.items():
|
for filepath, patterns in cfg.file_patterns.items():
|
||||||
for pattern in patterns:
|
for pattern in patterns:
|
||||||
cfg_str_parts.append(f"\n '{filepath}': '{pattern}'")
|
cfg_str_parts.append(f"\n '{filepath}': '{pattern.raw_pattern}',")
|
||||||
|
|
||||||
cfg_str_parts += ["\n})"]
|
cfg_str_parts += ["\n }\n)"]
|
||||||
return ", ".join(cfg_str_parts)
|
return "".join(cfg_str_parts)
|
||||||
|
|
||||||
|
|
||||||
MaybeConfig = typ.Optional[Config]
|
def _parse_cfg_file_patterns(
|
||||||
MaybeRawConfig = typ.Optional[RawConfig]
|
cfg_parser: configparser.RawConfigParser,
|
||||||
|
) -> typ.Iterable[FileRawPatternsItem]:
|
||||||
|
if not cfg_parser.has_section("pycalver:file_patterns"):
|
||||||
|
return
|
||||||
|
|
||||||
FilePatterns = typ.Dict[str, typ.List[str]]
|
file_pattern_items: typ.List[typ.Tuple[str, str]] = cfg_parser.items("pycalver:file_patterns")
|
||||||
|
|
||||||
|
|
||||||
def _parse_cfg_file_patterns(cfg_parser: configparser.RawConfigParser) -> FilePatterns:
|
|
||||||
file_patterns: FilePatterns = {}
|
|
||||||
|
|
||||||
file_pattern_items: typ.List[typ.Tuple[str, str]]
|
|
||||||
if cfg_parser.has_section("pycalver:file_patterns"):
|
|
||||||
file_pattern_items = cfg_parser.items("pycalver:file_patterns")
|
|
||||||
else:
|
|
||||||
file_pattern_items = []
|
|
||||||
|
|
||||||
for filepath, patterns_str in file_pattern_items:
|
for filepath, patterns_str in file_pattern_items:
|
||||||
patterns: typ.List[str] = []
|
maybe_patterns = (line.strip() for line in patterns_str.splitlines())
|
||||||
for line in patterns_str.splitlines():
|
patterns = [p for p in maybe_patterns if p]
|
||||||
pattern = line.strip()
|
yield filepath, patterns
|
||||||
if pattern:
|
|
||||||
patterns.append(pattern)
|
|
||||||
|
|
||||||
file_patterns[filepath] = patterns
|
|
||||||
|
|
||||||
return file_patterns
|
|
||||||
|
|
||||||
|
|
||||||
class _ConfigParser(configparser.RawConfigParser):
|
class _ConfigParser(configparser.RawConfigParser):
|
||||||
|
|
@ -178,7 +185,7 @@ def _parse_cfg(cfg_buffer: typ.IO[str]) -> RawConfig:
|
||||||
val = val.lower() in ("yes", "true", "1", "on")
|
val = val.lower() in ("yes", "true", "1", "on")
|
||||||
raw_cfg[option] = val
|
raw_cfg[option] = val
|
||||||
|
|
||||||
raw_cfg['file_patterns'] = _parse_cfg_file_patterns(cfg_parser)
|
raw_cfg['file_patterns'] = dict(_parse_cfg_file_patterns(cfg_parser))
|
||||||
|
|
||||||
return raw_cfg
|
return raw_cfg
|
||||||
|
|
||||||
|
|
@ -193,64 +200,66 @@ def _parse_toml(cfg_buffer: typ.IO[str]) -> RawConfig:
|
||||||
return raw_cfg
|
return raw_cfg
|
||||||
|
|
||||||
|
|
||||||
def _normalize_file_patterns(raw_cfg: RawConfig) -> FilePatterns:
|
def _iter_glob_expanded_file_patterns(
|
||||||
"""Create consistent representation of file_patterns.
|
raw_patterns_by_file: RawPatternsByFile,
|
||||||
|
) -> typ.Iterable[FileRawPatternsItem]:
|
||||||
|
for filepath_glob, raw_patterns in raw_patterns_by_file.items():
|
||||||
|
filepaths = glob.glob(filepath_glob)
|
||||||
|
if filepaths:
|
||||||
|
for filepath in filepaths:
|
||||||
|
yield filepath, raw_patterns
|
||||||
|
else:
|
||||||
|
logger.warning(f"Invalid config, no such file: {filepath_glob}")
|
||||||
|
# fallback to treating it as a simple path
|
||||||
|
yield filepath_glob, raw_patterns
|
||||||
|
|
||||||
|
|
||||||
|
def _compile_v1_file_patterns(raw_cfg: RawConfig) -> typ.Iterable[FilePatternsItem]:
|
||||||
|
"""Create inernal/compiled representation of the file_patterns config field.
|
||||||
|
|
||||||
The result the same, regardless of the config format.
|
The result the same, regardless of the config format.
|
||||||
"""
|
"""
|
||||||
version_str : str = raw_cfg['current_version']
|
# current_version: str = raw_cfg['current_version']
|
||||||
version_pattern: str = raw_cfg['version_pattern']
|
# current_pep440_version = version.pep440_version(current_version)
|
||||||
pep440_version : str = v1version.to_pep440(version_str)
|
|
||||||
|
|
||||||
file_patterns: FilePatterns
|
version_pattern : str = raw_cfg['version_pattern']
|
||||||
if 'file_patterns' in raw_cfg:
|
raw_patterns_by_file: RawPatternsByFile = raw_cfg['file_patterns']
|
||||||
file_patterns = raw_cfg['file_patterns']
|
|
||||||
else:
|
|
||||||
file_patterns = {}
|
|
||||||
|
|
||||||
for filepath_glob, patterns in list(file_patterns.items()):
|
for filepath, raw_patterns in _iter_glob_expanded_file_patterns(raw_patterns_by_file):
|
||||||
filepaths = glob.glob(filepath_glob)
|
compiled_patterns = [
|
||||||
if not filepaths:
|
v1patterns.compile_pattern(version_pattern, raw_pattern) for raw_pattern in raw_patterns
|
||||||
logger.warning(f"Invalid config, no such file: {filepath_glob}")
|
]
|
||||||
# fallback to treating it as a simple path
|
yield filepath, compiled_patterns
|
||||||
filepaths = [filepath_glob]
|
|
||||||
|
|
||||||
normalized_patterns: typ.List[str] = []
|
|
||||||
for pattern in patterns:
|
|
||||||
normalized_pattern = pattern.replace("{version}", version_pattern)
|
|
||||||
if version_pattern == "{pycalver}":
|
|
||||||
normalized_pattern = normalized_pattern.replace(
|
|
||||||
"{pep440_version}", "{pep440_pycalver}"
|
|
||||||
)
|
|
||||||
elif version_pattern == "{semver}":
|
|
||||||
normalized_pattern = normalized_pattern.replace("{pep440_version}", "{semver}")
|
|
||||||
elif "{pep440_version}" in pattern:
|
|
||||||
logger.warning(f"Invalid config, cannot match '{pattern}' for '{filepath_glob}'.")
|
|
||||||
logger.warning(f"No mapping of '{version_pattern}' to '{pep440_version}'")
|
|
||||||
normalized_patterns.append(normalized_pattern)
|
|
||||||
|
|
||||||
for filepath in filepaths:
|
def _compile_v2_file_patterns(raw_cfg: RawConfig) -> typ.Iterable[FilePatternsItem]:
|
||||||
file_patterns[filepath] = normalized_patterns
|
"""Create inernal/compiled representation of the file_patterns config field.
|
||||||
|
|
||||||
return file_patterns
|
The result the same, regardless of the config format.
|
||||||
|
"""
|
||||||
|
version_pattern : str = raw_cfg['version_pattern']
|
||||||
|
raw_patterns_by_file: RawPatternsByFile = raw_cfg['file_patterns']
|
||||||
|
|
||||||
|
for filepath, raw_patterns in _iter_glob_expanded_file_patterns(raw_patterns_by_file):
|
||||||
|
compiled_patterns = [
|
||||||
|
v2patterns.compile_pattern(version_pattern, raw_pattern) for raw_pattern in raw_patterns
|
||||||
|
]
|
||||||
|
yield filepath, compiled_patterns
|
||||||
|
|
||||||
|
|
||||||
def _parse_config(raw_cfg: RawConfig) -> Config:
|
def _parse_config(raw_cfg: RawConfig) -> Config:
|
||||||
"""Parse configuration which was loaded from an .ini/.cfg or .toml file."""
|
"""Parse configuration which was loaded from an .ini/.cfg or .toml file."""
|
||||||
|
|
||||||
if 'current_version' not in raw_cfg:
|
current_version: str = raw_cfg['current_version']
|
||||||
raise ValueError("Missing 'pycalver.current_version'")
|
current_version = raw_cfg['current_version'] = current_version.strip("'\" ")
|
||||||
|
|
||||||
version_str: str = raw_cfg['current_version']
|
version_pattern: str = raw_cfg['version_pattern']
|
||||||
version_str = raw_cfg['current_version'] = version_str.strip("'\" ")
|
|
||||||
|
|
||||||
version_pattern: str = raw_cfg.get('version_pattern', "{pycalver}")
|
|
||||||
version_pattern = raw_cfg['version_pattern'] = version_pattern.strip("'\" ")
|
version_pattern = raw_cfg['version_pattern'] = version_pattern.strip("'\" ")
|
||||||
|
|
||||||
commit_message: str = raw_cfg.get('commit_message', DEFAULT_COMMIT_MESSAGE)
|
commit_message: str = raw_cfg.get('commit_message', DEFAULT_COMMIT_MESSAGE)
|
||||||
commit_message = raw_cfg['commit_message'] = commit_message.strip("'\" ")
|
commit_message = raw_cfg['commit_message'] = commit_message.strip("'\" ")
|
||||||
|
|
||||||
is_new_pattern = not ("{" in version_pattern or "}" in version_pattern)
|
is_new_pattern = "{" not in version_pattern and "}" not in version_pattern
|
||||||
|
|
||||||
# TODO (mb 2020-09-18): Validate Pattern
|
# TODO (mb 2020-09-18): Validate Pattern
|
||||||
# detect YY with WW or UU -> suggest GG with VV
|
# detect YY with WW or UU -> suggest GG with VV
|
||||||
|
|
@ -260,11 +269,11 @@ def _parse_config(raw_cfg: RawConfig) -> Config:
|
||||||
# NOTE (mb 2019-01-05): Provoke ValueError if version_pattern
|
# NOTE (mb 2019-01-05): Provoke ValueError if version_pattern
|
||||||
# and current_version are not compatible.
|
# and current_version are not compatible.
|
||||||
if is_new_pattern:
|
if is_new_pattern:
|
||||||
v2version.parse_version_info(version_str, version_pattern)
|
v2version.parse_version_info(current_version, version_pattern)
|
||||||
else:
|
else:
|
||||||
v1version.parse_version_info(version_str, version_pattern)
|
v1version.parse_version_info(current_version, version_pattern)
|
||||||
|
|
||||||
pep440_version = v1version.to_pep440(version_str)
|
pep440_version = version.to_pep440(current_version)
|
||||||
|
|
||||||
commit = raw_cfg['commit']
|
commit = raw_cfg['commit']
|
||||||
tag = raw_cfg['tag']
|
tag = raw_cfg['tag']
|
||||||
|
|
@ -281,10 +290,13 @@ def _parse_config(raw_cfg: RawConfig) -> Config:
|
||||||
if push and not commit:
|
if push and not commit:
|
||||||
raise ValueError("pycalver.commit = true required if pycalver.push = true")
|
raise ValueError("pycalver.commit = true required if pycalver.push = true")
|
||||||
|
|
||||||
file_patterns = _normalize_file_patterns(raw_cfg)
|
if is_new_pattern:
|
||||||
|
file_patterns = dict(_compile_v2_file_patterns(raw_cfg))
|
||||||
|
else:
|
||||||
|
file_patterns = dict(_compile_v1_file_patterns(raw_cfg))
|
||||||
|
|
||||||
cfg = Config(
|
cfg = Config(
|
||||||
current_version=version_str,
|
current_version=current_version,
|
||||||
version_pattern=version_pattern,
|
version_pattern=version_pattern,
|
||||||
pep440_version=pep440_version,
|
pep440_version=pep440_version,
|
||||||
commit_message=commit_message,
|
commit_message=commit_message,
|
||||||
|
|
@ -298,11 +310,18 @@ def _parse_config(raw_cfg: RawConfig) -> Config:
|
||||||
return cfg
|
return cfg
|
||||||
|
|
||||||
|
|
||||||
def _parse_current_version_default_pattern(cfg: Config, raw_cfg_text: str) -> str:
|
def _parse_current_version_default_pattern(ctx: ProjectContext, raw_cfg: RawConfig) -> str:
|
||||||
|
fobj: typ.IO[str]
|
||||||
|
|
||||||
|
with ctx.config_filepath.open(mode="rt", encoding="utf-8") as fobj:
|
||||||
|
raw_cfg_text = fobj.read()
|
||||||
|
|
||||||
is_pycalver_section = False
|
is_pycalver_section = False
|
||||||
for line in raw_cfg_text.splitlines():
|
for line in raw_cfg_text.splitlines():
|
||||||
if is_pycalver_section and line.startswith("current_version"):
|
if is_pycalver_section and line.startswith("current_version"):
|
||||||
return line.replace(cfg.current_version, cfg.version_pattern)
|
current_version: str = raw_cfg['current_version']
|
||||||
|
version_pattern: str = raw_cfg['version_pattern']
|
||||||
|
return line.replace(current_version, version_pattern)
|
||||||
|
|
||||||
if line.strip() == "[pycalver]":
|
if line.strip() == "[pycalver]":
|
||||||
is_pycalver_section = True
|
is_pycalver_section = True
|
||||||
|
|
@ -312,44 +331,56 @@ def _parse_current_version_default_pattern(cfg: Config, raw_cfg_text: str) -> st
|
||||||
raise ValueError("Could not parse pycalver.current_version")
|
raise ValueError("Could not parse pycalver.current_version")
|
||||||
|
|
||||||
|
|
||||||
def parse(ctx: ProjectContext) -> MaybeConfig:
|
def _parse_raw_config(ctx: ProjectContext) -> RawConfig:
|
||||||
"""Parse config file if available."""
|
|
||||||
if not ctx.config_filepath.exists():
|
|
||||||
logger.warning(f"File not found: {ctx.config_filepath}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
fobj: typ.IO[str]
|
|
||||||
|
|
||||||
cfg_path: str
|
|
||||||
if ctx.config_filepath.is_absolute():
|
|
||||||
cfg_path = str(ctx.config_filepath.relative_to(ctx.path.absolute()))
|
|
||||||
else:
|
|
||||||
cfg_path = str(ctx.config_filepath)
|
|
||||||
|
|
||||||
raw_cfg: RawConfig
|
|
||||||
|
|
||||||
try:
|
|
||||||
with ctx.config_filepath.open(mode="rt", encoding="utf-8") as fobj:
|
with ctx.config_filepath.open(mode="rt", encoding="utf-8") as fobj:
|
||||||
if ctx.config_format == 'toml':
|
if ctx.config_format == 'toml':
|
||||||
raw_cfg = _parse_toml(fobj)
|
raw_cfg = _parse_toml(fobj)
|
||||||
elif ctx.config_format == 'cfg':
|
elif ctx.config_format == 'cfg':
|
||||||
raw_cfg = _parse_cfg(fobj)
|
raw_cfg = _parse_cfg(fobj)
|
||||||
else:
|
else:
|
||||||
err_msg = "Invalid config_format='{ctx.config_format}'"
|
err_msg = (
|
||||||
|
f"Invalid config_format='{ctx.config_format}'."
|
||||||
|
"Supported formats are 'setup.cfg' and 'pyproject.toml'"
|
||||||
|
)
|
||||||
raise RuntimeError(err_msg)
|
raise RuntimeError(err_msg)
|
||||||
|
|
||||||
cfg: Config = _parse_config(raw_cfg)
|
if 'current_version' in raw_cfg:
|
||||||
|
if not isinstance(raw_cfg['current_version'], str):
|
||||||
|
err = f"Invalid type for pycalver.current_version = {raw_cfg['current_version']}"
|
||||||
|
raise TypeError(err)
|
||||||
|
else:
|
||||||
|
raise ValueError("Missing 'pycalver.current_version'")
|
||||||
|
|
||||||
if cfg_path not in cfg.file_patterns:
|
if 'version_pattern' in raw_cfg:
|
||||||
fobj.seek(0)
|
if not isinstance(raw_cfg['version_pattern'], str):
|
||||||
raw_cfg_text = fobj.read()
|
err = f"Invalid type for pycalver.version_pattern = {raw_cfg['version_pattern']}"
|
||||||
cfg.file_patterns[cfg_path] = [
|
raise TypeError(err)
|
||||||
_parse_current_version_default_pattern(cfg, raw_cfg_text)
|
else:
|
||||||
]
|
raw_cfg['version_pattern'] = "{pycalver}"
|
||||||
|
|
||||||
return cfg
|
if 'file_patterns' not in raw_cfg:
|
||||||
except ValueError as ex:
|
raw_cfg['file_patterns'] = {}
|
||||||
logger.warning(f"Couldn't parse {cfg_path}: {str(ex)}")
|
|
||||||
|
if ctx.config_rel_path not in raw_cfg['file_patterns']:
|
||||||
|
# NOTE (mb 2020-09-19): By default we always add
|
||||||
|
# a pattern for the config section itself.
|
||||||
|
raw_version_pattern = _parse_current_version_default_pattern(ctx, raw_cfg)
|
||||||
|
raw_cfg['file_patterns'][ctx.config_rel_path] = [raw_version_pattern]
|
||||||
|
|
||||||
|
return raw_cfg
|
||||||
|
|
||||||
|
|
||||||
|
def parse(ctx: ProjectContext) -> MaybeConfig:
|
||||||
|
"""Parse config file if available."""
|
||||||
|
if not ctx.config_filepath.exists():
|
||||||
|
logger.warning(f"File not found: {ctx.config_rel_path}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
raw_cfg = _parse_raw_config(ctx)
|
||||||
|
return _parse_config(raw_cfg)
|
||||||
|
except (TypeError, ValueError) as ex:
|
||||||
|
logger.warning(f"Couldn't parse {ctx.config_rel_path}: {str(ex)}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -445,11 +476,11 @@ DEFAULT_TOML_README_MD_STR = """
|
||||||
|
|
||||||
|
|
||||||
def _initial_version() -> str:
|
def _initial_version() -> str:
|
||||||
return dt.datetime.now().strftime("v%Y%m.0001-alpha")
|
return dt.datetime.now().strftime("v%Y%m.1001-alpha")
|
||||||
|
|
||||||
|
|
||||||
def _initial_version_pep440() -> str:
|
def _initial_version_pep440() -> str:
|
||||||
return dt.datetime.now().strftime("%Y%m.1a0")
|
return dt.datetime.now().strftime("%Y%m.1001a0")
|
||||||
|
|
||||||
|
|
||||||
def default_config(ctx: ProjectContext) -> str:
|
def default_config(ctx: ProjectContext) -> str:
|
||||||
|
|
@ -506,4 +537,4 @@ def write_content(ctx: ProjectContext) -> None:
|
||||||
|
|
||||||
with ctx.config_filepath.open(mode="at", encoding="utf-8") as fobj:
|
with ctx.config_filepath.open(mode="at", encoding="utf-8") as fobj:
|
||||||
fobj.write(cfg_content)
|
fobj.write(cfg_content)
|
||||||
print(f"Updated {ctx.config_filepath}")
|
print(f"Updated {ctx.config_rel_path}")
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
import typing as typ
|
import typing as typ
|
||||||
|
|
||||||
import pycalver.patterns as v1patterns
|
from .patterns import Pattern
|
||||||
|
|
||||||
|
|
||||||
class PatternMatch(typ.NamedTuple):
|
class PatternMatch(typ.NamedTuple):
|
||||||
|
|
@ -15,7 +15,7 @@ class PatternMatch(typ.NamedTuple):
|
||||||
|
|
||||||
lineno : int # zero based
|
lineno : int # zero based
|
||||||
line : str
|
line : str
|
||||||
pattern: v1patterns.Pattern
|
pattern: Pattern
|
||||||
span : typ.Tuple[int, int]
|
span : typ.Tuple[int, int]
|
||||||
match : str
|
match : str
|
||||||
|
|
||||||
|
|
@ -23,25 +23,26 @@ class PatternMatch(typ.NamedTuple):
|
||||||
PatternMatches = typ.Iterable[PatternMatch]
|
PatternMatches = typ.Iterable[PatternMatch]
|
||||||
|
|
||||||
|
|
||||||
def _iter_for_pattern(lines: typ.List[str], pattern: v1patterns.Pattern) -> PatternMatches:
|
def _iter_for_pattern(lines: typ.List[str], pattern: Pattern) -> PatternMatches:
|
||||||
for lineno, line in enumerate(lines):
|
for lineno, line in enumerate(lines):
|
||||||
match = pattern.regexp.search(line)
|
match = pattern.regexp.search(line)
|
||||||
if match:
|
if match:
|
||||||
yield PatternMatch(lineno, line, pattern, match.span(), match.group(0))
|
yield PatternMatch(lineno, line, pattern, match.span(), match.group(0))
|
||||||
|
|
||||||
|
|
||||||
def iter_matches(lines: typ.List[str], patterns: typ.List[v1patterns.Pattern]) -> PatternMatches:
|
def iter_matches(lines: typ.List[str], patterns: typ.List[Pattern]) -> PatternMatches:
|
||||||
"""Iterate over all matches of any pattern on any line.
|
"""Iterate over all matches of any pattern on any line.
|
||||||
|
|
||||||
>>> import pycalver.patterns as v1patterns
|
>>> from . import v1patterns
|
||||||
>>> lines = ["__version__ = 'v201712.0002-alpha'"]
|
>>> lines = ["__version__ = 'v201712.0002-alpha'"]
|
||||||
>>> patterns = ["{pycalver}", "{pep440_pycalver}"]
|
>>> version_pattern = "{pycalver}"
|
||||||
>>> patterns = [v1patterns.compile_pattern(p) for p in patterns]
|
>>> raw_patterns = ["{pycalver}", "{pep440_pycalver}"]
|
||||||
|
>>> patterns = [v1patterns.compile_pattern(version_pattern, p) for p in raw_patterns]
|
||||||
>>> matches = list(iter_matches(lines, patterns))
|
>>> matches = list(iter_matches(lines, patterns))
|
||||||
>>> assert matches[0] == PatternMatch(
|
>>> assert matches[0] == PatternMatch(
|
||||||
... lineno = 0,
|
... lineno = 0,
|
||||||
... line = "__version__ = 'v201712.0002-alpha'",
|
... line = "__version__ = 'v201712.0002-alpha'",
|
||||||
... pattern= v1patterns.compile_pattern("{pycalver}"),
|
... pattern= v1patterns.compile_pattern(version_pattern),
|
||||||
... span = (15, 33),
|
... span = (15, 33),
|
||||||
... match = "v201712.0002-alpha",
|
... match = "v201712.0002-alpha",
|
||||||
... )
|
... )
|
||||||
|
|
|
||||||
|
|
@ -1,62 +1,14 @@
|
||||||
# This file is part of the pycalver project
|
|
||||||
# https://github.com/mbarkhau/pycalver
|
|
||||||
#
|
|
||||||
# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
|
|
||||||
# SPDX-License-Identifier: MIT
|
|
||||||
"""Compose Regular Expressions from Patterns.
|
|
||||||
|
|
||||||
>>> version_info = PYCALVER_RE.match("v201712.0123-alpha").groupdict()
|
|
||||||
>>> assert version_info == {
|
|
||||||
... "pycalver" : "v201712.0123-alpha",
|
|
||||||
... "vYYYYMM" : "v201712",
|
|
||||||
... "year" : "2017",
|
|
||||||
... "month" : "12",
|
|
||||||
... "build" : ".0123",
|
|
||||||
... "build_no" : "0123",
|
|
||||||
... "release" : "-alpha",
|
|
||||||
... "release_tag" : "alpha",
|
|
||||||
... }
|
|
||||||
>>>
|
|
||||||
>>> version_info = PYCALVER_RE.match("v201712.0033").groupdict()
|
|
||||||
>>> assert version_info == {
|
|
||||||
... "pycalver" : "v201712.0033",
|
|
||||||
... "vYYYYMM" : "v201712",
|
|
||||||
... "year" : "2017",
|
|
||||||
... "month" : "12",
|
|
||||||
... "build" : ".0033",
|
|
||||||
... "build_no" : "0033",
|
|
||||||
... "release" : None,
|
|
||||||
... "release_tag": None,
|
|
||||||
... }
|
|
||||||
"""
|
|
||||||
|
|
||||||
import re
|
|
||||||
import typing as typ
|
import typing as typ
|
||||||
|
|
||||||
# https://regex101.com/r/fnj60p/10
|
|
||||||
PYCALVER_PATTERN = r"""
|
|
||||||
\b
|
|
||||||
(?P<pycalver>
|
|
||||||
(?P<vYYYYMM>
|
|
||||||
v # "v" version prefix
|
|
||||||
(?P<year>\d{4})
|
|
||||||
(?P<month>\d{2})
|
|
||||||
)
|
|
||||||
(?P<build>
|
|
||||||
\. # "." build nr prefix
|
|
||||||
(?P<build_no>\d{4,})
|
|
||||||
)
|
|
||||||
(?P<release>
|
|
||||||
\- # "-" release prefix
|
|
||||||
(?P<release_tag>alpha|beta|dev|rc|post)
|
|
||||||
)?
|
|
||||||
)(?:\s|$)
|
|
||||||
"""
|
|
||||||
|
|
||||||
PYCALVER_RE: typ.Pattern[str] = re.compile(PYCALVER_PATTERN, flags=re.VERBOSE)
|
class Pattern(typ.NamedTuple):
|
||||||
|
|
||||||
|
version_pattern: str # "{pycalver}", "{year}.{month}", "vYYYY0M.BUILD"
|
||||||
|
raw_pattern : str # '__version__ = "{version}"', "Copyright (c) YYYY"
|
||||||
|
regexp : typ.Pattern[str]
|
||||||
|
|
||||||
|
|
||||||
PATTERN_ESCAPES = [
|
RE_PATTERN_ESCAPES = [
|
||||||
("\u005c", "\u005c\u005c"),
|
("\u005c", "\u005c\u005c"),
|
||||||
("-" , "\u005c-"),
|
("-" , "\u005c-"),
|
||||||
("." , "\u005c."),
|
("." , "\u005c."),
|
||||||
|
|
@ -70,158 +22,3 @@ PATTERN_ESCAPES = [
|
||||||
("(" , "\u005c("),
|
("(" , "\u005c("),
|
||||||
(")" , "\u005c)"),
|
(")" , "\u005c)"),
|
||||||
]
|
]
|
||||||
|
|
||||||
COMPOSITE_PART_PATTERNS = {
|
|
||||||
'pep440_pycalver': r"{year}{month}\.{BID}(?:{pep440_tag})?",
|
|
||||||
'pycalver' : r"v{year}{month}\.{bid}(?:-{tag})?",
|
|
||||||
'calver' : r"v{year}{month}",
|
|
||||||
'semver' : r"{MAJOR}\.{MINOR}\.{PATCH}",
|
|
||||||
'release_tag' : r"{tag}",
|
|
||||||
'build' : r"\.{bid}",
|
|
||||||
'release' : r"(?:-{tag})?",
|
|
||||||
# depricated
|
|
||||||
'pep440_version': r"{year}{month}\.{BID}(?:{pep440_tag})?",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
PART_PATTERNS = {
|
|
||||||
'year' : r"\d{4}",
|
|
||||||
'month' : r"(?:0[0-9]|1[0-2])",
|
|
||||||
'month_short': r"(?:1[0-2]|[1-9])",
|
|
||||||
'build_no' : r"\d{4,}",
|
|
||||||
'pep440_tag' : r"(?:a|b|dev|rc|post)?\d*",
|
|
||||||
'tag' : r"(?:alpha|beta|dev|rc|post|final)",
|
|
||||||
'yy' : r"\d{2}",
|
|
||||||
'yyyy' : r"\d{4}",
|
|
||||||
'quarter' : r"[1-4]",
|
|
||||||
'iso_week' : r"(?:[0-4]\d|5[0-3])",
|
|
||||||
'us_week' : r"(?:[0-4]\d|5[0-3])",
|
|
||||||
'dom' : r"(0[1-9]|[1-2][0-9]|3[0-1])",
|
|
||||||
'dom_short' : r"([1-9]|[1-2][0-9]|3[0-1])",
|
|
||||||
'doy' : r"(?:[0-2]\d\d|3[0-5][0-9]|36[0-6])",
|
|
||||||
'doy_short' : r"(?:[0-2]\d\d|3[0-5][0-9]|36[0-6])",
|
|
||||||
'MAJOR' : r"\d+",
|
|
||||||
'MINOR' : r"\d+",
|
|
||||||
'MM' : r"\d{2,}",
|
|
||||||
'MMM' : r"\d{3,}",
|
|
||||||
'MMMM' : r"\d{4,}",
|
|
||||||
'MMMMM' : r"\d{5,}",
|
|
||||||
'PATCH' : r"\d+",
|
|
||||||
'PP' : r"\d{2,}",
|
|
||||||
'PPP' : r"\d{3,}",
|
|
||||||
'PPPP' : r"\d{4,}",
|
|
||||||
'PPPPP' : r"\d{5,}",
|
|
||||||
'bid' : r"\d{4,}",
|
|
||||||
'BID' : r"[1-9]\d*",
|
|
||||||
'BB' : r"[1-9]\d{1,}",
|
|
||||||
'BBB' : r"[1-9]\d{2,}",
|
|
||||||
'BBBB' : r"[1-9]\d{3,}",
|
|
||||||
'BBBBB' : r"[1-9]\d{4,}",
|
|
||||||
'BBBBBB' : r"[1-9]\d{5,}",
|
|
||||||
'BBBBBBB' : r"[1-9]\d{6,}",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
PATTERN_PART_FIELDS = {
|
|
||||||
'year' : 'year',
|
|
||||||
'month' : 'month',
|
|
||||||
'month_short': 'month',
|
|
||||||
'pep440_tag' : 'tag',
|
|
||||||
'tag' : 'tag',
|
|
||||||
'yy' : 'year',
|
|
||||||
'yyyy' : 'year',
|
|
||||||
'quarter' : 'quarter',
|
|
||||||
'iso_week' : 'iso_week',
|
|
||||||
'us_week' : 'us_week',
|
|
||||||
'dom' : 'dom',
|
|
||||||
'doy' : 'doy',
|
|
||||||
'dom_short' : 'dom',
|
|
||||||
'doy_short' : 'doy',
|
|
||||||
'MAJOR' : 'major',
|
|
||||||
'MINOR' : 'minor',
|
|
||||||
'MM' : 'minor',
|
|
||||||
'MMM' : 'minor',
|
|
||||||
'MMMM' : 'minor',
|
|
||||||
'MMMMM' : 'minor',
|
|
||||||
'PP' : 'patch',
|
|
||||||
'PPP' : 'patch',
|
|
||||||
'PPPP' : 'patch',
|
|
||||||
'PPPPP' : 'patch',
|
|
||||||
'PATCH' : 'patch',
|
|
||||||
'build_no' : 'bid',
|
|
||||||
'bid' : 'bid',
|
|
||||||
'BID' : 'bid',
|
|
||||||
'BB' : 'bid',
|
|
||||||
'BBB' : 'bid',
|
|
||||||
'BBBB' : 'bid',
|
|
||||||
'BBBBB' : 'bid',
|
|
||||||
'BBBBBB' : 'bid',
|
|
||||||
'BBBBBBB' : 'bid',
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
FULL_PART_FORMATS = {
|
|
||||||
'pep440_pycalver': "{year}{month:02}.{BID}{pep440_tag}",
|
|
||||||
'pycalver' : "v{year}{month:02}.{bid}{release}",
|
|
||||||
'calver' : "v{year}{month:02}",
|
|
||||||
'semver' : "{MAJOR}.{MINOR}.{PATCH}",
|
|
||||||
'release_tag' : "{tag}",
|
|
||||||
'build' : ".{bid}",
|
|
||||||
# NOTE (mb 2019-01-04): since release is optional, it
|
|
||||||
# is treates specially in version.format
|
|
||||||
# 'release' : "-{tag}",
|
|
||||||
'month' : "{month:02}",
|
|
||||||
'month_short': "{month}",
|
|
||||||
'build_no' : "{bid}",
|
|
||||||
'iso_week' : "{iso_week:02}",
|
|
||||||
'us_week' : "{us_week:02}",
|
|
||||||
'dom' : "{dom:02}",
|
|
||||||
'doy' : "{doy:03}",
|
|
||||||
'dom_short' : "{dom}",
|
|
||||||
'doy_short' : "{doy}",
|
|
||||||
# depricated
|
|
||||||
'pep440_version': "{year}{month:02}.{BID}{pep440_tag}",
|
|
||||||
'version' : "v{year}{month:02}.{bid}{release}",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class Pattern(typ.NamedTuple):
|
|
||||||
|
|
||||||
raw : str # "{pycalver}", "{year}.{month}", "YYYY0M.BUILD"
|
|
||||||
regexp: typ.Pattern[str]
|
|
||||||
|
|
||||||
|
|
||||||
Patterns = typ.List[typ.Pattern[str]]
|
|
||||||
|
|
||||||
|
|
||||||
def _replace_pattern_parts(pattern: str) -> str:
|
|
||||||
# The pattern is escaped, so that everything besides the format
|
|
||||||
# string variables is treated literally.
|
|
||||||
for part_name, part_pattern in PART_PATTERNS.items():
|
|
||||||
named_part_pattern = f"(?P<{part_name}>{part_pattern})"
|
|
||||||
placeholder = "\u005c{" + part_name + "\u005c}"
|
|
||||||
pattern = pattern.replace(placeholder, named_part_pattern)
|
|
||||||
return pattern
|
|
||||||
|
|
||||||
|
|
||||||
def compile_pattern_str(pattern: str) -> str:
|
|
||||||
for char, escaped in PATTERN_ESCAPES:
|
|
||||||
pattern = pattern.replace(char, escaped)
|
|
||||||
|
|
||||||
return _replace_pattern_parts(pattern)
|
|
||||||
|
|
||||||
|
|
||||||
def compile_pattern(pattern: str) -> Pattern:
|
|
||||||
pattern_str = compile_pattern_str(pattern)
|
|
||||||
pattern_re = re.compile(pattern_str)
|
|
||||||
return Pattern(pattern, pattern_re)
|
|
||||||
|
|
||||||
|
|
||||||
def _init_composite_patterns() -> None:
|
|
||||||
for part_name, part_pattern in COMPOSITE_PART_PATTERNS.items():
|
|
||||||
part_pattern = part_pattern.replace("{", "\u005c{").replace("}", "\u005c}")
|
|
||||||
pattern_str = _replace_pattern_parts(part_pattern)
|
|
||||||
PART_PATTERNS[part_name] = pattern_str
|
|
||||||
|
|
||||||
|
|
||||||
_init_composite_patterns()
|
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,22 @@
|
||||||
# This file is part of the pycalver project
|
|
||||||
# https://github.com/mbarkhau/pycalver
|
|
||||||
#
|
|
||||||
# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
|
|
||||||
# SPDX-License-Identifier: MIT
|
|
||||||
"""Rewrite files, updating occurences of version strings."""
|
|
||||||
|
|
||||||
import io
|
|
||||||
import glob
|
|
||||||
import typing as typ
|
import typing as typ
|
||||||
import difflib
|
import difflib
|
||||||
import logging
|
|
||||||
|
|
||||||
import pathlib2 as pl
|
import pathlib2 as pl
|
||||||
|
|
||||||
import pycalver.version as v1version
|
from . import config
|
||||||
import pycalver.patterns as v1patterns
|
from .patterns import Pattern
|
||||||
from pycalver import parse
|
|
||||||
from pycalver import config
|
|
||||||
|
|
||||||
logger = logging.getLogger("pycalver.rewrite")
|
|
||||||
|
class NoPatternMatch(Exception):
|
||||||
|
"""Pattern not found in content.
|
||||||
|
|
||||||
|
logger.error is used to show error info about the patterns so
|
||||||
|
that users can debug what is wrong with them. The class
|
||||||
|
itself doesn't capture that info. This approach is used so
|
||||||
|
that all patter issues can be shown, rather than bubbling
|
||||||
|
all the way up the stack on the very first pattern with no
|
||||||
|
matches.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
def detect_line_sep(content: str) -> str:
|
def detect_line_sep(content: str) -> str:
|
||||||
|
|
@ -41,18 +39,6 @@ def detect_line_sep(content: str) -> str:
|
||||||
return "\n"
|
return "\n"
|
||||||
|
|
||||||
|
|
||||||
class NoPatternMatch(Exception):
|
|
||||||
"""Pattern not found in content.
|
|
||||||
|
|
||||||
logger.error is used to show error info about the patterns so
|
|
||||||
that users can debug what is wrong with them. The class
|
|
||||||
itself doesn't capture that info. This approach is used so
|
|
||||||
that all patter issues can be shown, rather than bubbling
|
|
||||||
all the way up the stack on the very first pattern with no
|
|
||||||
matches.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class RewrittenFileData(typ.NamedTuple):
|
class RewrittenFileData(typ.NamedTuple):
|
||||||
"""Container for line-wise content of rewritten files."""
|
"""Container for line-wise content of rewritten files."""
|
||||||
|
|
||||||
|
|
@ -62,117 +48,19 @@ class RewrittenFileData(typ.NamedTuple):
|
||||||
new_lines: typ.List[str]
|
new_lines: typ.List[str]
|
||||||
|
|
||||||
|
|
||||||
def iter_file_paths(
|
PathPatternsItem = typ.Tuple[pl.Path, typ.List[Pattern]]
|
||||||
file_patterns: config.PatternsByGlob,
|
|
||||||
) -> typ.Iterable[typ.Tuple[pl.Path, config.Patterns]]:
|
|
||||||
for globstr, pattern_strs in file_patterns.items():
|
|
||||||
file_paths = glob.glob(globstr)
|
|
||||||
if not any(file_paths):
|
|
||||||
errmsg = f"No files found for path/glob '{globstr}'"
|
|
||||||
raise IOError(errmsg)
|
|
||||||
for file_path_str in file_paths:
|
|
||||||
file_path = pl.Path(file_path_str)
|
|
||||||
yield (file_path, pattern_strs)
|
|
||||||
|
|
||||||
|
|
||||||
def rewrite_lines(
|
def iter_path_patterns_items(
|
||||||
pattern_strs: typ.List[str],
|
file_patterns: config.PatternsByFile,
|
||||||
new_vinfo : v1version.VersionInfo,
|
) -> typ.Iterable[PathPatternsItem]:
|
||||||
old_lines : typ.List[str],
|
for filepath_str, patterns in file_patterns.items():
|
||||||
) -> typ.List[str]:
|
filepath_obj = pl.Path(filepath_str)
|
||||||
"""Replace occurances of pattern_strs in old_lines with new_vinfo.
|
if filepath_obj.exists():
|
||||||
|
yield (filepath_obj, patterns)
|
||||||
>>> new_vinfo = v1version.parse_version_info("v201811.0123-beta")
|
|
||||||
>>> pattern_strs = ['__version__ = "{pycalver}"']
|
|
||||||
>>> rewrite_lines(pattern_strs, new_vinfo, ['__version__ = "v201809.0002-beta"'])
|
|
||||||
['__version__ = "v201811.0123-beta"']
|
|
||||||
|
|
||||||
>>> pattern_strs = ['__version__ = "{pep440_version}"']
|
|
||||||
>>> rewrite_lines(pattern_strs, new_vinfo, ['__version__ = "201809.2b0"'])
|
|
||||||
['__version__ = "201811.123b0"']
|
|
||||||
"""
|
|
||||||
new_lines = old_lines[:]
|
|
||||||
found_patterns = set()
|
|
||||||
|
|
||||||
patterns = [v1patterns.compile_pattern(p) for p in pattern_strs]
|
|
||||||
matches = parse.iter_matches(old_lines, patterns)
|
|
||||||
for match in matches:
|
|
||||||
found_patterns.add(match.pattern.raw)
|
|
||||||
replacement = v1version.format_version(new_vinfo, match.pattern.raw)
|
|
||||||
span_l, span_r = match.span
|
|
||||||
new_line = match.line[:span_l] + replacement + match.line[span_r:]
|
|
||||||
new_lines[match.lineno] = new_line
|
|
||||||
|
|
||||||
non_matched_patterns = set(pattern_strs) - found_patterns
|
|
||||||
if non_matched_patterns:
|
|
||||||
for non_matched_pattern in non_matched_patterns:
|
|
||||||
logger.error(f"No match for pattern '{non_matched_pattern}'")
|
|
||||||
compiled_pattern_str = v1patterns.compile_pattern_str(non_matched_pattern)
|
|
||||||
logger.error(f"Pattern compiles to regex '{compiled_pattern_str}'")
|
|
||||||
raise NoPatternMatch("Invalid pattern(s)")
|
|
||||||
else:
|
else:
|
||||||
return new_lines
|
errmsg = f"File does not exist: '{filepath_str}'"
|
||||||
|
raise IOError(errmsg)
|
||||||
|
|
||||||
def rfd_from_content(
|
|
||||||
pattern_strs: typ.List[str],
|
|
||||||
new_vinfo : v1version.VersionInfo,
|
|
||||||
content : str,
|
|
||||||
) -> RewrittenFileData:
|
|
||||||
r"""Rewrite pattern occurrences with version string.
|
|
||||||
|
|
||||||
>>> new_vinfo = v1version.parse_version_info("v201809.0123")
|
|
||||||
>>> pattern_strs = ['__version__ = "{pycalver}"']
|
|
||||||
>>> content = '__version__ = "v201809.0001-alpha"'
|
|
||||||
>>> rfd = rfd_from_content(pattern_strs, new_vinfo, content)
|
|
||||||
>>> rfd.new_lines
|
|
||||||
['__version__ = "v201809.0123"']
|
|
||||||
>>>
|
|
||||||
>>> new_vinfo = v1version.parse_version_info("v1.2.3", "v{semver}")
|
|
||||||
>>> pattern_strs = ['__version__ = "v{semver}"']
|
|
||||||
>>> content = '__version__ = "v1.2.2"'
|
|
||||||
>>> rfd = rfd_from_content(pattern_strs, new_vinfo, content)
|
|
||||||
>>> rfd.new_lines
|
|
||||||
['__version__ = "v1.2.3"']
|
|
||||||
"""
|
|
||||||
line_sep = detect_line_sep(content)
|
|
||||||
old_lines = content.split(line_sep)
|
|
||||||
new_lines = rewrite_lines(pattern_strs, new_vinfo, old_lines)
|
|
||||||
return RewrittenFileData("<path>", line_sep, old_lines, new_lines)
|
|
||||||
|
|
||||||
|
|
||||||
def iter_rewritten(
|
|
||||||
file_patterns: config.PatternsByGlob,
|
|
||||||
new_vinfo : v1version.VersionInfo,
|
|
||||||
) -> typ.Iterable[RewrittenFileData]:
|
|
||||||
r'''Iterate over files with version string replaced.
|
|
||||||
|
|
||||||
>>> file_patterns = {"src/pycalver/__init__.py": ['__version__ = "{pycalver}"']}
|
|
||||||
>>> new_vinfo = v1version.parse_version_info("v201809.0123")
|
|
||||||
>>> rewritten_datas = iter_rewritten(file_patterns, new_vinfo)
|
|
||||||
>>> rfd = list(rewritten_datas)[0]
|
|
||||||
>>> expected = [
|
|
||||||
... '# This file is part of the pycalver project',
|
|
||||||
... '# https://github.com/mbarkhau/pycalver',
|
|
||||||
... '#',
|
|
||||||
... '# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License',
|
|
||||||
... '# SPDX-License-Identifier: MIT',
|
|
||||||
... '"""PyCalVer: CalVer for Python Packages."""',
|
|
||||||
... '',
|
|
||||||
... '__version__ = "v201809.0123"',
|
|
||||||
... '',
|
|
||||||
... ]
|
|
||||||
>>> assert rfd.new_lines[:len(expected)] == expected
|
|
||||||
'''
|
|
||||||
|
|
||||||
fobj: typ.IO[str]
|
|
||||||
|
|
||||||
for file_path, pattern_strs in iter_file_paths(file_patterns):
|
|
||||||
with file_path.open(mode="rt", encoding="utf-8") as fobj:
|
|
||||||
content = fobj.read()
|
|
||||||
|
|
||||||
rfd = rfd_from_content(pattern_strs, new_vinfo, content)
|
|
||||||
yield rfd._replace(path=str(file_path))
|
|
||||||
|
|
||||||
|
|
||||||
def diff_lines(rfd: RewrittenFileData) -> typ.List[str]:
|
def diff_lines(rfd: RewrittenFileData) -> typ.List[str]:
|
||||||
|
|
@ -188,57 +76,10 @@ def diff_lines(rfd: RewrittenFileData) -> typ.List[str]:
|
||||||
['--- <path>', '+++ <path>', '@@ -1 +1 @@', '-foo', '+bar']
|
['--- <path>', '+++ <path>', '@@ -1 +1 @@', '-foo', '+bar']
|
||||||
"""
|
"""
|
||||||
lines = difflib.unified_diff(
|
lines = difflib.unified_diff(
|
||||||
a=rfd.old_lines, b=rfd.new_lines, lineterm="", fromfile=rfd.path, tofile=rfd.path
|
a=rfd.old_lines,
|
||||||
|
b=rfd.new_lines,
|
||||||
|
lineterm="",
|
||||||
|
fromfile=rfd.path,
|
||||||
|
tofile=rfd.path,
|
||||||
)
|
)
|
||||||
return list(lines)
|
return list(lines)
|
||||||
|
|
||||||
|
|
||||||
def diff(new_vinfo: v1version.VersionInfo, file_patterns: config.PatternsByGlob) -> str:
|
|
||||||
r"""Generate diffs of rewritten files.
|
|
||||||
|
|
||||||
>>> new_vinfo = v1version.parse_version_info("v201809.0123")
|
|
||||||
>>> file_patterns = {"src/pycalver/__init__.py": ['__version__ = "{pycalver}"']}
|
|
||||||
>>> diff_str = diff(new_vinfo, file_patterns)
|
|
||||||
>>> lines = diff_str.split("\n")
|
|
||||||
>>> lines[:2]
|
|
||||||
['--- src/pycalver/__init__.py', '+++ src/pycalver/__init__.py']
|
|
||||||
>>> assert lines[6].startswith('-__version__ = "v2')
|
|
||||||
>>> assert not lines[6].startswith('-__version__ = "v201809.0123"')
|
|
||||||
>>> lines[7]
|
|
||||||
'+__version__ = "v201809.0123"'
|
|
||||||
"""
|
|
||||||
|
|
||||||
full_diff = ""
|
|
||||||
fobj: typ.IO[str]
|
|
||||||
|
|
||||||
for file_path, pattern_strs in sorted(iter_file_paths(file_patterns)):
|
|
||||||
with file_path.open(mode="rt", encoding="utf-8") as fobj:
|
|
||||||
content = fobj.read()
|
|
||||||
|
|
||||||
try:
|
|
||||||
rfd = rfd_from_content(pattern_strs, new_vinfo, content)
|
|
||||||
except NoPatternMatch:
|
|
||||||
# pylint:disable=raise-missing-from ; we support py2, so not an option
|
|
||||||
errmsg = f"No patterns matched for '{file_path}'"
|
|
||||||
raise NoPatternMatch(errmsg)
|
|
||||||
|
|
||||||
rfd = rfd._replace(path=str(file_path))
|
|
||||||
lines = diff_lines(rfd)
|
|
||||||
if len(lines) == 0:
|
|
||||||
errmsg = f"No patterns matched for '{file_path}'"
|
|
||||||
raise NoPatternMatch(errmsg)
|
|
||||||
|
|
||||||
full_diff += "\n".join(lines) + "\n"
|
|
||||||
|
|
||||||
full_diff = full_diff.rstrip("\n")
|
|
||||||
return full_diff
|
|
||||||
|
|
||||||
|
|
||||||
def rewrite(file_patterns: config.PatternsByGlob, new_vinfo: v1version.VersionInfo) -> None:
|
|
||||||
"""Rewrite project files, updating each with the new version."""
|
|
||||||
fobj: typ.IO[str]
|
|
||||||
|
|
||||||
for file_data in iter_rewritten(file_patterns, new_vinfo):
|
|
||||||
new_content = file_data.line_sep.join(file_data.new_lines)
|
|
||||||
with io.open(file_data.path, mode="wt", encoding="utf-8") as fobj:
|
|
||||||
fobj.write(new_content)
|
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,12 @@ Provided subcommands: show, test, init, bump
|
||||||
import typing as typ
|
import typing as typ
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import pycalver.rewrite as v1rewrite
|
from . import config
|
||||||
import pycalver.version as v1version
|
from . import version
|
||||||
from pycalver import config
|
from . import v1rewrite
|
||||||
|
from . import v1version
|
||||||
|
|
||||||
logger = logging.getLogger("pycalver.cli")
|
logger = logging.getLogger("pycalver.v1cli")
|
||||||
|
|
||||||
|
|
||||||
def update_cfg_from_vcs(cfg: config.Config, all_tags: typ.List[str]) -> config.Config:
|
def update_cfg_from_vcs(cfg: config.Config, all_tags: typ.List[str]) -> config.Config:
|
||||||
|
|
@ -28,7 +29,7 @@ def update_cfg_from_vcs(cfg: config.Config, all_tags: typ.List[str]) -> config.C
|
||||||
version_tags.sort(reverse=True)
|
version_tags.sort(reverse=True)
|
||||||
logger.debug(f"found {len(version_tags)} tags: {version_tags[:2]}")
|
logger.debug(f"found {len(version_tags)} tags: {version_tags[:2]}")
|
||||||
latest_version_tag = version_tags[0]
|
latest_version_tag = version_tags[0]
|
||||||
latest_version_pep440 = v1version.to_pep440(latest_version_tag)
|
latest_version_pep440 = version.to_pep440(latest_version_tag)
|
||||||
if latest_version_tag <= cfg.current_version:
|
if latest_version_tag <= cfg.current_version:
|
||||||
return cfg
|
return cfg
|
||||||
|
|
||||||
|
|
@ -40,14 +41,15 @@ def update_cfg_from_vcs(cfg: config.Config, all_tags: typ.List[str]) -> config.C
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def rewrite(
|
def rewrite_files(
|
||||||
cfg : config.Config,
|
cfg : config.Config,
|
||||||
new_version: str,
|
new_version: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
new_vinfo = v1version.parse_version_info(new_version, cfg.version_pattern)
|
new_vinfo = v1version.parse_version_info(new_version, cfg.version_pattern)
|
||||||
v1rewrite.rewrite(cfg.file_patterns, new_vinfo)
|
v1rewrite.rewrite_files(cfg.file_patterns, new_vinfo)
|
||||||
|
|
||||||
|
|
||||||
def get_diff(cfg: config.Config, new_version: str) -> str:
|
def get_diff(cfg: config.Config, new_version: str) -> str:
|
||||||
new_vinfo = v1version.parse_version_info(new_version, cfg.version_pattern)
|
old_vinfo = v1version.parse_version_info(cfg.current_version, cfg.version_pattern)
|
||||||
return v1rewrite.diff(new_vinfo, cfg.file_patterns)
|
new_vinfo = v1version.parse_version_info(new_version , cfg.version_pattern)
|
||||||
|
return v1rewrite.diff(old_vinfo, new_vinfo, cfg.file_patterns)
|
||||||
220
src/pycalver/v1patterns.py
Normal file
220
src/pycalver/v1patterns.py
Normal file
|
|
@ -0,0 +1,220 @@
|
||||||
|
# This file is part of the pycalver project
|
||||||
|
# https://github.com/mbarkhau/pycalver
|
||||||
|
#
|
||||||
|
# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
"""Compose Regular Expressions from Patterns.
|
||||||
|
|
||||||
|
>>> version_info = PYCALVER_RE.match("v201712.0123-alpha").groupdict()
|
||||||
|
>>> assert version_info == {
|
||||||
|
... "pycalver" : "v201712.0123-alpha",
|
||||||
|
... "vYYYYMM" : "v201712",
|
||||||
|
... "year" : "2017",
|
||||||
|
... "month" : "12",
|
||||||
|
... "build" : ".0123",
|
||||||
|
... "build_no" : "0123",
|
||||||
|
... "release" : "-alpha",
|
||||||
|
... "release_tag" : "alpha",
|
||||||
|
... }
|
||||||
|
>>>
|
||||||
|
>>> version_info = PYCALVER_RE.match("v201712.0033").groupdict()
|
||||||
|
>>> assert version_info == {
|
||||||
|
... "pycalver" : "v201712.0033",
|
||||||
|
... "vYYYYMM" : "v201712",
|
||||||
|
... "year" : "2017",
|
||||||
|
... "month" : "12",
|
||||||
|
... "build" : ".0033",
|
||||||
|
... "build_no" : "0033",
|
||||||
|
... "release" : None,
|
||||||
|
... "release_tag": None,
|
||||||
|
... }
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import typing as typ
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from .patterns import RE_PATTERN_ESCAPES
|
||||||
|
from .patterns import Pattern
|
||||||
|
|
||||||
|
logger = logging.getLogger("pycalver.v1patterns")
|
||||||
|
|
||||||
|
# https://regex101.com/r/fnj60p/10
|
||||||
|
PYCALVER_PATTERN = r"""
|
||||||
|
\b
|
||||||
|
(?P<pycalver>
|
||||||
|
(?P<vYYYYMM>
|
||||||
|
v # "v" version prefix
|
||||||
|
(?P<year>\d{4})
|
||||||
|
(?P<month>\d{2})
|
||||||
|
)
|
||||||
|
(?P<build>
|
||||||
|
\. # "." build nr prefix
|
||||||
|
(?P<build_no>\d{4,})
|
||||||
|
)
|
||||||
|
(?P<release>
|
||||||
|
\- # "-" release prefix
|
||||||
|
(?P<release_tag>alpha|beta|dev|rc|post)
|
||||||
|
)?
|
||||||
|
)(?:\s|$)
|
||||||
|
"""
|
||||||
|
|
||||||
|
PYCALVER_RE: typ.Pattern[str] = re.compile(PYCALVER_PATTERN, flags=re.VERBOSE)
|
||||||
|
|
||||||
|
|
||||||
|
COMPOSITE_PART_PATTERNS = {
|
||||||
|
'pep440_pycalver': r"{year}{month}\.{BID}(?:{pep440_tag})?",
|
||||||
|
'pycalver' : r"v{year}{month}\.{bid}(?:-{tag})?",
|
||||||
|
'calver' : r"v{year}{month}",
|
||||||
|
'semver' : r"{MAJOR}\.{MINOR}\.{PATCH}",
|
||||||
|
'release_tag' : r"{tag}",
|
||||||
|
'build' : r"\.{bid}",
|
||||||
|
'release' : r"(?:-{tag})?",
|
||||||
|
# depricated
|
||||||
|
'pep440_version': r"{year}{month}\.{BID}(?:{pep440_tag})?",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
PART_PATTERNS = {
|
||||||
|
'year' : r"\d{4}",
|
||||||
|
'month' : r"(?:0[0-9]|1[0-2])",
|
||||||
|
'month_short': r"(?:1[0-2]|[1-9])",
|
||||||
|
'build_no' : r"\d{4,}",
|
||||||
|
'pep440_tag' : r"(?:a|b|dev|rc|post)?\d*",
|
||||||
|
'tag' : r"(?:alpha|beta|dev|rc|post|final)",
|
||||||
|
'yy' : r"\d{2}",
|
||||||
|
'yyyy' : r"\d{4}",
|
||||||
|
'quarter' : r"[1-4]",
|
||||||
|
'iso_week' : r"(?:[0-4]\d|5[0-3])",
|
||||||
|
'us_week' : r"(?:[0-4]\d|5[0-3])",
|
||||||
|
'dom' : r"(0[1-9]|[1-2][0-9]|3[0-1])",
|
||||||
|
'dom_short' : r"([1-9]|[1-2][0-9]|3[0-1])",
|
||||||
|
'doy' : r"(?:[0-2]\d\d|3[0-5][0-9]|36[0-6])",
|
||||||
|
'doy_short' : r"(?:[0-2]\d\d|3[0-5][0-9]|36[0-6])",
|
||||||
|
'MAJOR' : r"\d+",
|
||||||
|
'MINOR' : r"\d+",
|
||||||
|
'MM' : r"\d{2,}",
|
||||||
|
'MMM' : r"\d{3,}",
|
||||||
|
'MMMM' : r"\d{4,}",
|
||||||
|
'MMMMM' : r"\d{5,}",
|
||||||
|
'PATCH' : r"\d+",
|
||||||
|
'PP' : r"\d{2,}",
|
||||||
|
'PPP' : r"\d{3,}",
|
||||||
|
'PPPP' : r"\d{4,}",
|
||||||
|
'PPPPP' : r"\d{5,}",
|
||||||
|
'bid' : r"\d{4,}",
|
||||||
|
'BID' : r"[1-9]\d*",
|
||||||
|
'BB' : r"[1-9]\d{1,}",
|
||||||
|
'BBB' : r"[1-9]\d{2,}",
|
||||||
|
'BBBB' : r"[1-9]\d{3,}",
|
||||||
|
'BBBBB' : r"[1-9]\d{4,}",
|
||||||
|
'BBBBBB' : r"[1-9]\d{5,}",
|
||||||
|
'BBBBBBB' : r"[1-9]\d{6,}",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
PATTERN_PART_FIELDS = {
|
||||||
|
'year' : 'year',
|
||||||
|
'month' : 'month',
|
||||||
|
'month_short': 'month',
|
||||||
|
'pep440_tag' : 'tag',
|
||||||
|
'tag' : 'tag',
|
||||||
|
'yy' : 'year',
|
||||||
|
'yyyy' : 'year',
|
||||||
|
'quarter' : 'quarter',
|
||||||
|
'iso_week' : 'iso_week',
|
||||||
|
'us_week' : 'us_week',
|
||||||
|
'dom' : 'dom',
|
||||||
|
'doy' : 'doy',
|
||||||
|
'dom_short' : 'dom',
|
||||||
|
'doy_short' : 'doy',
|
||||||
|
'MAJOR' : 'major',
|
||||||
|
'MINOR' : 'minor',
|
||||||
|
'MM' : 'minor',
|
||||||
|
'MMM' : 'minor',
|
||||||
|
'MMMM' : 'minor',
|
||||||
|
'MMMMM' : 'minor',
|
||||||
|
'PP' : 'patch',
|
||||||
|
'PPP' : 'patch',
|
||||||
|
'PPPP' : 'patch',
|
||||||
|
'PPPPP' : 'patch',
|
||||||
|
'PATCH' : 'patch',
|
||||||
|
'build_no' : 'bid',
|
||||||
|
'bid' : 'bid',
|
||||||
|
'BID' : 'bid',
|
||||||
|
'BB' : 'bid',
|
||||||
|
'BBB' : 'bid',
|
||||||
|
'BBBB' : 'bid',
|
||||||
|
'BBBBB' : 'bid',
|
||||||
|
'BBBBBB' : 'bid',
|
||||||
|
'BBBBBBB' : 'bid',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
FULL_PART_FORMATS = {
|
||||||
|
'pep440_pycalver': "{year}{month:02}.{BID}{pep440_tag}",
|
||||||
|
'pycalver' : "v{year}{month:02}.{bid}{release}",
|
||||||
|
'calver' : "v{year}{month:02}",
|
||||||
|
'semver' : "{MAJOR}.{MINOR}.{PATCH}",
|
||||||
|
'release_tag' : "{tag}",
|
||||||
|
'build' : ".{bid}",
|
||||||
|
# NOTE (mb 2019-01-04): since release is optional, it
|
||||||
|
# is treates specially in version.format
|
||||||
|
# 'release' : "-{tag}",
|
||||||
|
'month' : "{month:02}",
|
||||||
|
'month_short': "{month}",
|
||||||
|
'build_no' : "{bid}",
|
||||||
|
'iso_week' : "{iso_week:02}",
|
||||||
|
'us_week' : "{us_week:02}",
|
||||||
|
'dom' : "{dom:02}",
|
||||||
|
'doy' : "{doy:03}",
|
||||||
|
'dom_short' : "{dom}",
|
||||||
|
'doy_short' : "{doy}",
|
||||||
|
# depricated
|
||||||
|
'pep440_version': "{year}{month:02}.{BID}{pep440_tag}",
|
||||||
|
'version' : "v{year}{month:02}.{bid}{release}",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _replace_pattern_parts(pattern: str) -> str:
|
||||||
|
# The pattern is escaped, so that everything besides the format
|
||||||
|
# string variables is treated literally.
|
||||||
|
for part_name, part_pattern in PART_PATTERNS.items():
|
||||||
|
named_part_pattern = f"(?P<{part_name}>{part_pattern})"
|
||||||
|
placeholder = "\u005c{" + part_name + "\u005c}"
|
||||||
|
pattern = pattern.replace(placeholder, named_part_pattern)
|
||||||
|
return pattern
|
||||||
|
|
||||||
|
|
||||||
|
def _init_composite_patterns() -> None:
|
||||||
|
for part_name, part_pattern in COMPOSITE_PART_PATTERNS.items():
|
||||||
|
part_pattern = part_pattern.replace("{", "\u005c{").replace("}", "\u005c}")
|
||||||
|
pattern_str = _replace_pattern_parts(part_pattern)
|
||||||
|
PART_PATTERNS[part_name] = pattern_str
|
||||||
|
|
||||||
|
|
||||||
|
_init_composite_patterns()
|
||||||
|
|
||||||
|
|
||||||
|
def _compile_pattern_re(version_pattern: str, raw_pattern: str) -> typ.Pattern[str]:
|
||||||
|
normalized_pattern = raw_pattern.replace(r"{version}", version_pattern)
|
||||||
|
if version_pattern == r"{pycalver}":
|
||||||
|
normalized_pattern = normalized_pattern.replace(r"{pep440_version}", r"{pep440_pycalver}")
|
||||||
|
elif version_pattern == r"{semver}":
|
||||||
|
normalized_pattern = normalized_pattern.replace(r"{pep440_version}", r"{semver}")
|
||||||
|
elif r"{pep440_version}" in raw_pattern:
|
||||||
|
logger.warning(f"No mapping of '{version_pattern}' to '{{pep440_version}}'")
|
||||||
|
|
||||||
|
escaped_pattern = normalized_pattern
|
||||||
|
for char, escaped in RE_PATTERN_ESCAPES:
|
||||||
|
escaped_pattern = escaped_pattern.replace(char, escaped)
|
||||||
|
|
||||||
|
# TODO (mb 2020-09-19): replace {version} etc with version_pattern
|
||||||
|
pattern_str = _replace_pattern_parts(escaped_pattern)
|
||||||
|
return re.compile(pattern_str)
|
||||||
|
|
||||||
|
|
||||||
|
def compile_pattern(version_pattern: str, raw_pattern: typ.Optional[str] = None) -> Pattern:
|
||||||
|
_raw_pattern = version_pattern if raw_pattern is None else raw_pattern
|
||||||
|
regexp = _compile_pattern_re(version_pattern, _raw_pattern)
|
||||||
|
return Pattern(version_pattern, _raw_pattern, regexp)
|
||||||
193
src/pycalver/v1rewrite.py
Normal file
193
src/pycalver/v1rewrite.py
Normal file
|
|
@ -0,0 +1,193 @@
|
||||||
|
# This file is part of the pycalver project
|
||||||
|
# https://github.com/mbarkhau/pycalver
|
||||||
|
#
|
||||||
|
# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
"""Rewrite files, updating occurences of version strings."""
|
||||||
|
|
||||||
|
import io
|
||||||
|
import typing as typ
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from . import parse
|
||||||
|
from . import config
|
||||||
|
from . import rewrite
|
||||||
|
from . import version
|
||||||
|
from . import v1version
|
||||||
|
from .patterns import Pattern
|
||||||
|
|
||||||
|
logger = logging.getLogger("pycalver.v1rewrite")
|
||||||
|
|
||||||
|
|
||||||
|
def rewrite_lines(
|
||||||
|
patterns : typ.List[Pattern],
|
||||||
|
new_vinfo: version.V1VersionInfo,
|
||||||
|
old_lines: typ.List[str],
|
||||||
|
) -> typ.List[str]:
|
||||||
|
"""Replace occurances of patterns in old_lines with new_vinfo.
|
||||||
|
|
||||||
|
>>> from .v1patterns import compile_pattern
|
||||||
|
>>> version_pattern = "{pycalver}"
|
||||||
|
>>> new_vinfo = v1version.parse_version_info("v201811.0123-beta", version_pattern)
|
||||||
|
>>> patterns = [compile_pattern(version_pattern, '__version__ = "{pycalver}"')]
|
||||||
|
>>> rewrite_lines(patterns, new_vinfo, ['__version__ = "v201809.0002-beta"'])
|
||||||
|
['__version__ = "v201811.0123-beta"']
|
||||||
|
|
||||||
|
>>> patterns = [compile_pattern(version_pattern, '__version__ = "{pep440_version}"')]
|
||||||
|
>>> rewrite_lines(patterns, new_vinfo, ['__version__ = "201809.2b0"'])
|
||||||
|
['__version__ = "201811.123b0"']
|
||||||
|
"""
|
||||||
|
new_lines = old_lines[:]
|
||||||
|
found_patterns = set()
|
||||||
|
|
||||||
|
for match in parse.iter_matches(old_lines, patterns):
|
||||||
|
found_patterns.add(match.pattern.raw_pattern)
|
||||||
|
replacement = v1version.format_version(new_vinfo, match.pattern.raw_pattern)
|
||||||
|
span_l, span_r = match.span
|
||||||
|
new_line = match.line[:span_l] + replacement + match.line[span_r:]
|
||||||
|
new_lines[match.lineno] = new_line
|
||||||
|
|
||||||
|
non_matched_patterns = set(patterns) - found_patterns
|
||||||
|
if non_matched_patterns:
|
||||||
|
for nmp in non_matched_patterns:
|
||||||
|
logger.error(f"No match for pattern '{nmp.raw_pattern}'")
|
||||||
|
logger.error(f"Pattern compiles to regex '{nmp.regexp.pattern}'")
|
||||||
|
raise rewrite.NoPatternMatch("Invalid pattern(s)")
|
||||||
|
else:
|
||||||
|
return new_lines
|
||||||
|
|
||||||
|
|
||||||
|
def rfd_from_content(
|
||||||
|
patterns : typ.List[Pattern],
|
||||||
|
new_vinfo: version.V1VersionInfo,
|
||||||
|
content : str,
|
||||||
|
path : str = "<path>",
|
||||||
|
) -> rewrite.RewrittenFileData:
|
||||||
|
r"""Rewrite pattern occurrences with version string.
|
||||||
|
|
||||||
|
>>> from .v1patterns import compile_pattern
|
||||||
|
>>> patterns = [compile_pattern("{pycalver}", '__version__ = "{pycalver}"']
|
||||||
|
>>> new_vinfo = v1version.parse_version_info("v201809.0123")
|
||||||
|
|
||||||
|
>>> content = '__version__ = "v201809.0001-alpha"'
|
||||||
|
>>> rfd = rfd_from_content(patterns, new_vinfo, content)
|
||||||
|
>>> rfd.new_lines
|
||||||
|
['__version__ = "v201809.0123"']
|
||||||
|
|
||||||
|
>>> patterns = [compile_pattern('{semver}', '__version__ = "v{semver}"')]
|
||||||
|
>>> new_vinfo = v1version.parse_version_info("v1.2.3", "v{semver}")
|
||||||
|
|
||||||
|
>>> content = '__version__ = "v1.2.2"'
|
||||||
|
>>> rfd = rfd_from_content(patterns, new_vinfo, content)
|
||||||
|
>>> rfd.new_lines
|
||||||
|
['__version__ = "v1.2.3"']
|
||||||
|
"""
|
||||||
|
line_sep = rewrite.detect_line_sep(content)
|
||||||
|
old_lines = content.split(line_sep)
|
||||||
|
new_lines = rewrite_lines(patterns, new_vinfo, old_lines)
|
||||||
|
return rewrite.RewrittenFileData(path, line_sep, old_lines, new_lines)
|
||||||
|
|
||||||
|
|
||||||
|
def iter_rewritten(
|
||||||
|
file_patterns: config.PatternsByFile,
|
||||||
|
new_vinfo : version.V1VersionInfo,
|
||||||
|
) -> typ.Iterable[rewrite.RewrittenFileData]:
|
||||||
|
r'''Iterate over files with version string replaced.
|
||||||
|
|
||||||
|
>>> version_pattern = "{pycalver}"
|
||||||
|
>>> file_patterns = {"src/pycalver/__init__.py": ['__version__ = "{pycalver}"']}
|
||||||
|
>>> new_vinfo = v1version.parse_version_info("v201809.0123")
|
||||||
|
>>> rewritten_datas = iter_rewritten(version_pattern, file_patterns, new_vinfo)
|
||||||
|
>>> rfd = list(rewritten_datas)[0]
|
||||||
|
>>> expected = [
|
||||||
|
... '# This file is part of the pycalver project',
|
||||||
|
... '# https://github.com/mbarkhau/pycalver',
|
||||||
|
... '#',
|
||||||
|
... '# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License',
|
||||||
|
... '# SPDX-License-Identifier: MIT',
|
||||||
|
... '"""PyCalVer: CalVer for Python Packages."""',
|
||||||
|
... '',
|
||||||
|
... '__version__ = "v201809.0123"',
|
||||||
|
... '',
|
||||||
|
... ]
|
||||||
|
>>> assert rfd.new_lines == expected
|
||||||
|
'''
|
||||||
|
|
||||||
|
fobj: typ.IO[str]
|
||||||
|
|
||||||
|
for file_path, pattern_strs in rewrite.iter_path_patterns_items(file_patterns):
|
||||||
|
with file_path.open(mode="rt", encoding="utf-8") as fobj:
|
||||||
|
content = fobj.read()
|
||||||
|
|
||||||
|
rfd = rfd_from_content(pattern_strs, new_vinfo, content)
|
||||||
|
yield rfd._replace(path=str(file_path))
|
||||||
|
|
||||||
|
|
||||||
|
def diff(
|
||||||
|
old_vinfo : version.V1VersionInfo,
|
||||||
|
new_vinfo : version.V1VersionInfo,
|
||||||
|
file_patterns: config.PatternsByFile,
|
||||||
|
) -> str:
|
||||||
|
r"""Generate diffs of rewritten files.
|
||||||
|
|
||||||
|
>>> old_vinfo = v1version.parse_version_info("v201809.0123")
|
||||||
|
>>> new_vinfo = v1version.parse_version_info("v201810.1124")
|
||||||
|
>>> file_patterns = {"src/pycalver/__init__.py": ['__version__ = "{pycalver}"']}
|
||||||
|
>>> diff_str = diff(old_vinfo, new_vinfo, file_patterns)
|
||||||
|
>>> lines = diff_str.split("\n")
|
||||||
|
>>> lines[:2]
|
||||||
|
['--- src/pycalver/__init__.py', '+++ src/pycalver/__init__.py']
|
||||||
|
>>> assert lines[6].startswith('-__version__ = "v2')
|
||||||
|
>>> assert not lines[6].startswith('-__version__ = "v201809.0123"')
|
||||||
|
>>> lines[7]
|
||||||
|
'+__version__ = "v201809.0123"'
|
||||||
|
|
||||||
|
>>> file_patterns = {"LICENSE": ['Copyright (c) 2018-{year}']}
|
||||||
|
>>> diff_str = diff(old_vinfo, new_vinfo, file_patterns)
|
||||||
|
>>> assert not diff_str
|
||||||
|
"""
|
||||||
|
|
||||||
|
full_diff = ""
|
||||||
|
fobj: typ.IO[str]
|
||||||
|
|
||||||
|
for file_path, patterns in sorted(rewrite.iter_path_patterns_items(file_patterns)):
|
||||||
|
with file_path.open(mode="rt", encoding="utf-8") as fobj:
|
||||||
|
content = fobj.read()
|
||||||
|
|
||||||
|
has_updated_version = False
|
||||||
|
for pattern in patterns:
|
||||||
|
old_str = v1version.format_version(old_vinfo, pattern.raw_pattern)
|
||||||
|
new_str = v1version.format_version(new_vinfo, pattern.raw_pattern)
|
||||||
|
if old_str != new_str:
|
||||||
|
has_updated_version = True
|
||||||
|
|
||||||
|
try:
|
||||||
|
rfd = rfd_from_content(patterns, new_vinfo, content)
|
||||||
|
except rewrite.NoPatternMatch:
|
||||||
|
# pylint:disable=raise-missing-from ; we support py2, so not an option
|
||||||
|
errmsg = f"No patterns matched for '{file_path}'"
|
||||||
|
raise rewrite.NoPatternMatch(errmsg)
|
||||||
|
|
||||||
|
rfd = rfd._replace(path=str(file_path))
|
||||||
|
lines = rewrite.diff_lines(rfd)
|
||||||
|
if len(lines) == 0 and has_updated_version:
|
||||||
|
errmsg = f"No patterns matched for '{file_path}'"
|
||||||
|
raise rewrite.NoPatternMatch(errmsg)
|
||||||
|
|
||||||
|
full_diff += "\n".join(lines) + "\n"
|
||||||
|
|
||||||
|
full_diff = full_diff.rstrip("\n")
|
||||||
|
return full_diff
|
||||||
|
|
||||||
|
|
||||||
|
def rewrite_files(
|
||||||
|
file_patterns: config.PatternsByFile,
|
||||||
|
new_vinfo : version.V1VersionInfo,
|
||||||
|
) -> None:
|
||||||
|
"""Rewrite project files, updating each with the new version."""
|
||||||
|
fobj: typ.IO[str]
|
||||||
|
|
||||||
|
for file_data in iter_rewritten(file_patterns, new_vinfo):
|
||||||
|
new_content = file_data.line_sep.join(file_data.new_lines)
|
||||||
|
with io.open(file_data.path, mode="wt", encoding="utf-8") as fobj:
|
||||||
|
fobj.write(new_content)
|
||||||
407
src/pycalver/v1version.py
Normal file
407
src/pycalver/v1version.py
Normal file
|
|
@ -0,0 +1,407 @@
|
||||||
|
# This file is part of the pycalver project
|
||||||
|
# https://github.com/mbarkhau/pycalver
|
||||||
|
#
|
||||||
|
# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
"""Functions related to version string manipulation."""
|
||||||
|
|
||||||
|
import typing as typ
|
||||||
|
import logging
|
||||||
|
import datetime as dt
|
||||||
|
|
||||||
|
from . import version
|
||||||
|
from . import v1patterns
|
||||||
|
|
||||||
|
logger = logging.getLogger("pycalver.v1version")
|
||||||
|
|
||||||
|
|
||||||
|
CalInfo = typ.Union[version.V1CalendarInfo, version.V1VersionInfo]
|
||||||
|
|
||||||
|
|
||||||
|
def _is_later_than(old: CalInfo, new: CalInfo) -> bool:
|
||||||
|
"""Is old > new based on non None fields."""
|
||||||
|
for field in version.V1CalendarInfo._fields:
|
||||||
|
aval = getattr(old, field)
|
||||||
|
bval = getattr(new, field)
|
||||||
|
if not (aval is None or bval is None):
|
||||||
|
if aval > bval:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _ver_to_cal_info(vnfo: version.V1VersionInfo) -> version.V1CalendarInfo:
|
||||||
|
return version.V1CalendarInfo(
|
||||||
|
vnfo.year,
|
||||||
|
vnfo.quarter,
|
||||||
|
vnfo.month,
|
||||||
|
vnfo.dom,
|
||||||
|
vnfo.doy,
|
||||||
|
vnfo.iso_week,
|
||||||
|
vnfo.us_week,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def cal_info(date: dt.date = None) -> version.V1CalendarInfo:
|
||||||
|
"""Generate calendar components for current date.
|
||||||
|
|
||||||
|
>>> from datetime import date
|
||||||
|
|
||||||
|
>>> c = cal_info(date(2019, 1, 5))
|
||||||
|
>>> (c.year, c.quarter, c.month, c.dom, c.doy, c.iso_week, c.us_week)
|
||||||
|
(2019, 1, 1, 5, 5, 0, 0)
|
||||||
|
|
||||||
|
>>> c = cal_info(date(2019, 1, 6))
|
||||||
|
>>> (c.year, c.quarter, c.month, c.dom, c.doy, c.iso_week, c.us_week)
|
||||||
|
(2019, 1, 1, 6, 6, 0, 1)
|
||||||
|
|
||||||
|
>>> c = cal_info(date(2019, 1, 7))
|
||||||
|
>>> (c.year, c.quarter, c.month, c.dom, c.doy, c.iso_week, c.us_week)
|
||||||
|
(2019, 1, 1, 7, 7, 1, 1)
|
||||||
|
|
||||||
|
>>> c = cal_info(date(2019, 4, 7))
|
||||||
|
>>> (c.year, c.quarter, c.month, c.dom, c.doy, c.iso_week, c.us_week)
|
||||||
|
(2019, 2, 4, 7, 97, 13, 14)
|
||||||
|
"""
|
||||||
|
if date is None:
|
||||||
|
date = version.TODAY
|
||||||
|
|
||||||
|
kwargs = {
|
||||||
|
'year' : date.year,
|
||||||
|
'quarter' : version.quarter_from_month(date.month),
|
||||||
|
'month' : date.month,
|
||||||
|
'dom' : date.day,
|
||||||
|
'doy' : int(date.strftime("%j"), base=10),
|
||||||
|
'iso_week': int(date.strftime("%W"), base=10),
|
||||||
|
'us_week' : int(date.strftime("%U"), base=10),
|
||||||
|
}
|
||||||
|
|
||||||
|
return version.V1CalendarInfo(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
FieldKey = str
|
||||||
|
MatchGroupKey = str
|
||||||
|
MatchGroupStr = str
|
||||||
|
|
||||||
|
PatternGroups = typ.Dict[MatchGroupKey, MatchGroupStr]
|
||||||
|
FieldValues = typ.Dict[FieldKey , MatchGroupStr]
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_field_values(field_values: FieldValues) -> version.V1VersionInfo:
|
||||||
|
fvals = field_values
|
||||||
|
tag = fvals.get('tag')
|
||||||
|
if tag is None:
|
||||||
|
tag = "final"
|
||||||
|
tag = version.TAG_BY_PEP440_TAG.get(tag, tag)
|
||||||
|
assert tag is not None
|
||||||
|
|
||||||
|
bid = fvals['bid'] if 'bid' in fvals else "0001"
|
||||||
|
|
||||||
|
year = int(fvals['year']) if 'year' in fvals else None
|
||||||
|
doy = int(fvals['doy' ]) if 'doy' in fvals else None
|
||||||
|
|
||||||
|
month: typ.Optional[int]
|
||||||
|
dom : typ.Optional[int]
|
||||||
|
|
||||||
|
if year and doy:
|
||||||
|
date = version.date_from_doy(year, doy)
|
||||||
|
month = date.month
|
||||||
|
dom = date.day
|
||||||
|
else:
|
||||||
|
month = int(fvals['month']) if 'month' in fvals else None
|
||||||
|
dom = int(fvals['dom' ]) if 'dom' in fvals else None
|
||||||
|
|
||||||
|
iso_week: typ.Optional[int]
|
||||||
|
us_week : typ.Optional[int]
|
||||||
|
|
||||||
|
if year and month and dom:
|
||||||
|
date = dt.date(year, month, dom)
|
||||||
|
doy = int(date.strftime("%j"), base=10)
|
||||||
|
iso_week = int(date.strftime("%W"), base=10)
|
||||||
|
us_week = int(date.strftime("%U"), base=10)
|
||||||
|
else:
|
||||||
|
iso_week = None
|
||||||
|
us_week = None
|
||||||
|
|
||||||
|
quarter = int(fvals['quarter']) if 'quarter' in fvals else None
|
||||||
|
if quarter is None and month:
|
||||||
|
quarter = version.quarter_from_month(month)
|
||||||
|
|
||||||
|
major = int(fvals['major']) if 'major' in fvals else 0
|
||||||
|
minor = int(fvals['minor']) if 'minor' in fvals else 0
|
||||||
|
patch = int(fvals['patch']) if 'patch' in fvals else 0
|
||||||
|
|
||||||
|
return version.V1VersionInfo(
|
||||||
|
year=year,
|
||||||
|
quarter=quarter,
|
||||||
|
month=month,
|
||||||
|
dom=dom,
|
||||||
|
doy=doy,
|
||||||
|
iso_week=iso_week,
|
||||||
|
us_week=us_week,
|
||||||
|
major=major,
|
||||||
|
minor=minor,
|
||||||
|
patch=patch,
|
||||||
|
bid=bid,
|
||||||
|
tag=tag,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_calver(cinfo: CalInfo) -> bool:
|
||||||
|
"""Check pattern for any calendar based parts.
|
||||||
|
|
||||||
|
>>> _is_calver(cal_info())
|
||||||
|
True
|
||||||
|
|
||||||
|
>>> vnfo = _parse_version_info({'year': "2018", 'month': "11", 'bid': "0018"})
|
||||||
|
>>> _is_calver(vnfo)
|
||||||
|
True
|
||||||
|
|
||||||
|
>>> vnfo = _parse_version_info({'MAJOR': "1", 'MINOR': "023", 'PATCH': "45"})
|
||||||
|
>>> _is_calver(vnfo)
|
||||||
|
False
|
||||||
|
"""
|
||||||
|
for field in version.V1CalendarInfo._fields:
|
||||||
|
maybe_val: typ.Any = getattr(cinfo, field, None)
|
||||||
|
if isinstance(maybe_val, int):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
VersionInfoKW = typ.Dict[str, typ.Union[str, int, None]]
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_pattern_groups(pattern_groups: PatternGroups) -> FieldValues:
|
||||||
|
for part_name in pattern_groups.keys():
|
||||||
|
is_valid_part_name = (
|
||||||
|
part_name in v1patterns.COMPOSITE_PART_PATTERNS
|
||||||
|
or part_name in v1patterns.PATTERN_PART_FIELDS
|
||||||
|
)
|
||||||
|
if not is_valid_part_name:
|
||||||
|
err_msg = f"Invalid part '{part_name}'"
|
||||||
|
raise version.PatternError(err_msg)
|
||||||
|
|
||||||
|
field_value_items = [
|
||||||
|
(field_name, pattern_groups[part_name])
|
||||||
|
for part_name, field_name in v1patterns.PATTERN_PART_FIELDS.items()
|
||||||
|
if part_name in pattern_groups.keys()
|
||||||
|
]
|
||||||
|
|
||||||
|
all_fields = [field_name for field_name, _ in field_value_items]
|
||||||
|
unique_fields = set(all_fields)
|
||||||
|
duplicate_fields = [f for f in unique_fields if all_fields.count(f) > 1]
|
||||||
|
|
||||||
|
if any(duplicate_fields):
|
||||||
|
err_msg = f"Multiple parts for same field {duplicate_fields}."
|
||||||
|
raise version.PatternError(err_msg)
|
||||||
|
else:
|
||||||
|
return dict(field_value_items)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_version_info(pattern_groups: PatternGroups) -> version.V1VersionInfo:
|
||||||
|
"""Parse normalized V1VersionInfo from groups of a matched pattern.
|
||||||
|
|
||||||
|
>>> vnfo = _parse_version_info({'year': "2018", 'month': "11", 'bid': "0099"})
|
||||||
|
>>> (vnfo.year, vnfo.month, vnfo.quarter, vnfo.bid, vnfo.tag)
|
||||||
|
(2018, 11, 4, '0099', 'final')
|
||||||
|
|
||||||
|
>>> vnfo = _parse_version_info({'year': "2018", 'doy': "11", 'bid': "099", 'tag': "b"})
|
||||||
|
>>> (vnfo.year, vnfo.month, vnfo.dom, vnfo.bid, vnfo.tag)
|
||||||
|
(2018, 1, 11, '099', 'beta')
|
||||||
|
|
||||||
|
>>> vnfo = _parse_version_info({'MAJOR': "1", 'MINOR': "23", 'PATCH': "45"})
|
||||||
|
>>> (vnfo.major, vnfo.minor, vnfo.patch)
|
||||||
|
(1, 23, 45)
|
||||||
|
|
||||||
|
>>> vnfo = _parse_version_info({'MAJOR': "1", 'MMM': "023", 'PPPP': "0045"})
|
||||||
|
>>> (vnfo.major, vnfo.minor, vnfo.patch)
|
||||||
|
(1, 23, 45)
|
||||||
|
"""
|
||||||
|
field_values = _parse_pattern_groups(pattern_groups)
|
||||||
|
return _parse_field_values(field_values)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_version_info(version_str: str, raw_pattern: str = "{pycalver}") -> version.V1VersionInfo:
|
||||||
|
"""Parse normalized V1VersionInfo.
|
||||||
|
|
||||||
|
>>> vnfo = parse_version_info("v201712.0033-beta", raw_pattern="{pycalver}")
|
||||||
|
>>> assert vnfo == _parse_version_info({'year': 2017, 'month': 12, 'bid': "0033", 'tag': "beta"})
|
||||||
|
|
||||||
|
>>> vnfo = parse_version_info("1.23.456", raw_pattern="{semver}")
|
||||||
|
>>> assert vnfo == _parse_version_info({'MAJOR': "1", 'MINOR': "23", 'PATCH': "456"})
|
||||||
|
"""
|
||||||
|
pattern = v1patterns.compile_pattern(raw_pattern)
|
||||||
|
match = pattern.regexp.match(version_str)
|
||||||
|
if match is None:
|
||||||
|
err_msg = (
|
||||||
|
f"Invalid version string '{version_str}' "
|
||||||
|
f"for pattern '{raw_pattern}'/'{pattern.regexp}'"
|
||||||
|
)
|
||||||
|
raise version.PatternError(err_msg)
|
||||||
|
else:
|
||||||
|
return _parse_version_info(match.groupdict())
|
||||||
|
|
||||||
|
|
||||||
|
def is_valid(version_str: str, raw_pattern: str = "{pycalver}") -> bool:
|
||||||
|
"""Check if a version matches a pattern.
|
||||||
|
|
||||||
|
>>> is_valid("v201712.0033-beta", raw_pattern="{pycalver}")
|
||||||
|
True
|
||||||
|
>>> is_valid("v201712.0033-beta", raw_pattern="{semver}")
|
||||||
|
False
|
||||||
|
>>> is_valid("1.2.3", raw_pattern="{semver}")
|
||||||
|
True
|
||||||
|
>>> is_valid("v201712.0033-beta", raw_pattern="{semver}")
|
||||||
|
False
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
parse_version_info(version_str, raw_pattern)
|
||||||
|
return True
|
||||||
|
except version.PatternError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
ID_FIELDS_BY_PART = {
|
||||||
|
'MAJOR' : 'major',
|
||||||
|
'MINOR' : 'minor',
|
||||||
|
'MM' : 'minor',
|
||||||
|
'MMM' : 'minor',
|
||||||
|
'MMMM' : 'minor',
|
||||||
|
'MMMMM' : 'minor',
|
||||||
|
'MMMMMM' : 'minor',
|
||||||
|
'MMMMMMM': 'minor',
|
||||||
|
'PATCH' : 'patch',
|
||||||
|
'PP' : 'patch',
|
||||||
|
'PPP' : 'patch',
|
||||||
|
'PPPP' : 'patch',
|
||||||
|
'PPPPP' : 'patch',
|
||||||
|
'PPPPPP' : 'patch',
|
||||||
|
'PPPPPPP': 'patch',
|
||||||
|
'BID' : 'bid',
|
||||||
|
'BB' : 'bid',
|
||||||
|
'BBB' : 'bid',
|
||||||
|
'BBBB' : 'bid',
|
||||||
|
'BBBBB' : 'bid',
|
||||||
|
'BBBBBB' : 'bid',
|
||||||
|
'BBBBBBB': 'bid',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def format_version(vinfo: version.V1VersionInfo, raw_pattern: str) -> str:
|
||||||
|
"""Generate version string.
|
||||||
|
|
||||||
|
>>> import datetime as dt
|
||||||
|
>>> vinfo = parse_version_info("v201712.0033-beta", raw_pattern="{pycalver}")
|
||||||
|
>>> vinfo_a = vinfo._replace(**cal_info(date=dt.date(2017, 1, 1))._asdict())
|
||||||
|
>>> vinfo_b = vinfo._replace(**cal_info(date=dt.date(2017, 12, 31))._asdict())
|
||||||
|
>>> vinfo_c = vinfo_b._replace(major=1, minor=2, patch=34, tag='final')
|
||||||
|
|
||||||
|
>>> format_version(vinfo_a, raw_pattern="v{yy}.{BID}{release}")
|
||||||
|
'v17.33-beta'
|
||||||
|
>>> format_version(vinfo_a, raw_pattern="{pep440_version}")
|
||||||
|
'201701.33b0'
|
||||||
|
|
||||||
|
>>> format_version(vinfo_a, raw_pattern="{pycalver}")
|
||||||
|
'v201701.0033-beta'
|
||||||
|
>>> format_version(vinfo_b, raw_pattern="{pycalver}")
|
||||||
|
'v201712.0033-beta'
|
||||||
|
|
||||||
|
>>> format_version(vinfo_a, raw_pattern="v{year}w{iso_week}.{BID}{release}")
|
||||||
|
'v2017w00.33-beta'
|
||||||
|
>>> format_version(vinfo_b, raw_pattern="v{year}w{iso_week}.{BID}{release}")
|
||||||
|
'v2017w52.33-beta'
|
||||||
|
|
||||||
|
>>> format_version(vinfo_a, raw_pattern="v{year}d{doy}.{bid}{release}")
|
||||||
|
'v2017d001.0033-beta'
|
||||||
|
>>> format_version(vinfo_b, raw_pattern="v{year}d{doy}.{bid}{release}")
|
||||||
|
'v2017d365.0033-beta'
|
||||||
|
|
||||||
|
>>> format_version(vinfo_c, raw_pattern="v{year}w{iso_week}.{BID}-{tag}")
|
||||||
|
'v2017w52.33-final'
|
||||||
|
>>> format_version(vinfo_c, raw_pattern="v{year}w{iso_week}.{BID}{release}")
|
||||||
|
'v2017w52.33'
|
||||||
|
|
||||||
|
>>> format_version(vinfo_c, raw_pattern="v{MAJOR}.{MINOR}.{PATCH}")
|
||||||
|
'v1.2.34'
|
||||||
|
>>> format_version(vinfo_c, raw_pattern="v{MAJOR}.{MM}.{PPP}")
|
||||||
|
'v1.02.034'
|
||||||
|
"""
|
||||||
|
full_pattern = raw_pattern
|
||||||
|
for part_name, full_part_format in v1patterns.FULL_PART_FORMATS.items():
|
||||||
|
full_pattern = full_pattern.replace("{" + part_name + "}", full_part_format)
|
||||||
|
|
||||||
|
kwargs: typ.Dict[str, typ.Union[str, int, None]] = vinfo._asdict()
|
||||||
|
|
||||||
|
tag = vinfo.tag
|
||||||
|
if tag == 'final':
|
||||||
|
kwargs['release' ] = ""
|
||||||
|
kwargs['pep440_tag'] = ""
|
||||||
|
else:
|
||||||
|
kwargs['release' ] = "-" + tag
|
||||||
|
kwargs['pep440_tag'] = version.PEP440_TAG_BY_TAG[tag] + "0"
|
||||||
|
|
||||||
|
kwargs['release_tag'] = tag
|
||||||
|
|
||||||
|
year = vinfo.year
|
||||||
|
if year:
|
||||||
|
kwargs['yy' ] = str(year)[-2:]
|
||||||
|
kwargs['yyyy'] = year
|
||||||
|
|
||||||
|
kwargs['BID'] = int(vinfo.bid, 10)
|
||||||
|
|
||||||
|
for part_name, field in ID_FIELDS_BY_PART.items():
|
||||||
|
val = kwargs[field]
|
||||||
|
if part_name.lower() == field.lower():
|
||||||
|
if isinstance(val, str):
|
||||||
|
kwargs[part_name] = int(val, base=10)
|
||||||
|
else:
|
||||||
|
kwargs[part_name] = val
|
||||||
|
else:
|
||||||
|
assert len(set(part_name)) == 1
|
||||||
|
padded_len = len(part_name)
|
||||||
|
kwargs[part_name] = str(val).zfill(padded_len)
|
||||||
|
|
||||||
|
return full_pattern.format(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def incr(
|
||||||
|
old_version: str,
|
||||||
|
raw_pattern: str = "{pycalver}",
|
||||||
|
*,
|
||||||
|
release : typ.Optional[str] = None,
|
||||||
|
major : bool = False,
|
||||||
|
minor : bool = False,
|
||||||
|
patch : bool = False,
|
||||||
|
pin_date: bool = False,
|
||||||
|
) -> typ.Optional[str]:
|
||||||
|
"""Increment version string.
|
||||||
|
|
||||||
|
'old_version' is assumed to be a string that matches 'pattern'
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
old_vinfo = parse_version_info(old_version, raw_pattern)
|
||||||
|
except version.PatternError as ex:
|
||||||
|
logger.error(str(ex))
|
||||||
|
return None
|
||||||
|
|
||||||
|
cur_cinfo = _ver_to_cal_info(old_vinfo) if pin_date else cal_info()
|
||||||
|
|
||||||
|
if _is_later_than(old_vinfo, cur_cinfo):
|
||||||
|
cur_vinfo = old_vinfo._replace(**cur_cinfo._asdict())
|
||||||
|
else:
|
||||||
|
logger.warning(f"Version appears to be from the future '{old_version}'")
|
||||||
|
cur_vinfo = old_vinfo
|
||||||
|
|
||||||
|
cur_vinfo = version.incr_non_cal_parts(
|
||||||
|
cur_vinfo,
|
||||||
|
release,
|
||||||
|
major,
|
||||||
|
minor,
|
||||||
|
patch,
|
||||||
|
)
|
||||||
|
new_version = format_version(cur_vinfo, raw_pattern)
|
||||||
|
if new_version == old_version:
|
||||||
|
logger.error("Invalid arguments or pattern, version did not change.")
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return new_version
|
||||||
|
|
@ -12,12 +12,12 @@ Provided subcommands: show, test, init, bump
|
||||||
import typing as typ
|
import typing as typ
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import pycalver.version as v1version
|
from . import config
|
||||||
import pycalver2.rewrite as v2rewrite
|
from . import version
|
||||||
import pycalver2.version as v2version
|
from . import v2rewrite
|
||||||
from pycalver import config
|
from . import v2version
|
||||||
|
|
||||||
logger = logging.getLogger("pycalver2.cli")
|
logger = logging.getLogger("pycalver.v2cli")
|
||||||
|
|
||||||
|
|
||||||
def update_cfg_from_vcs(cfg: config.Config, all_tags: typ.List[str]) -> config.Config:
|
def update_cfg_from_vcs(cfg: config.Config, all_tags: typ.List[str]) -> config.Config:
|
||||||
|
|
@ -29,7 +29,7 @@ def update_cfg_from_vcs(cfg: config.Config, all_tags: typ.List[str]) -> config.C
|
||||||
version_tags.sort(reverse=True)
|
version_tags.sort(reverse=True)
|
||||||
logger.debug(f"found {len(version_tags)} tags: {version_tags[:2]}")
|
logger.debug(f"found {len(version_tags)} tags: {version_tags[:2]}")
|
||||||
latest_version_tag = version_tags[0]
|
latest_version_tag = version_tags[0]
|
||||||
latest_version_pep440 = v1version.to_pep440(latest_version_tag)
|
latest_version_pep440 = version.to_pep440(latest_version_tag)
|
||||||
if latest_version_tag <= cfg.current_version:
|
if latest_version_tag <= cfg.current_version:
|
||||||
return cfg
|
return cfg
|
||||||
|
|
||||||
|
|
@ -41,14 +41,15 @@ def update_cfg_from_vcs(cfg: config.Config, all_tags: typ.List[str]) -> config.C
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def rewrite(
|
def rewrite_files(
|
||||||
cfg : config.Config,
|
cfg : config.Config,
|
||||||
new_version: str,
|
new_version: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
new_vinfo = v2version.parse_version_info(new_version, cfg.version_pattern)
|
new_vinfo = v2version.parse_version_info(new_version, cfg.version_pattern)
|
||||||
v2rewrite.rewrite(cfg.file_patterns, new_vinfo)
|
v2rewrite.rewrite_files(cfg.file_patterns, new_vinfo)
|
||||||
|
|
||||||
|
|
||||||
def get_diff(cfg: config.Config, new_version: str) -> str:
|
def get_diff(cfg: config.Config, new_version: str) -> str:
|
||||||
new_vinfo = v2version.parse_version_info(new_version, cfg.version_pattern)
|
old_vinfo = v2version.parse_version_info(cfg.current_version, cfg.version_pattern)
|
||||||
return v2rewrite.diff(new_vinfo, cfg.file_patterns)
|
new_vinfo = v2version.parse_version_info(new_version , cfg.version_pattern)
|
||||||
|
return v2rewrite.diff(old_vinfo, new_vinfo, cfg.file_patterns)
|
||||||
|
|
@ -33,30 +33,28 @@
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import typing as typ
|
import typing as typ
|
||||||
|
import logging
|
||||||
|
|
||||||
import pycalver.patterns as v1patterns
|
from .patterns import RE_PATTERN_ESCAPES
|
||||||
|
from .patterns import Pattern
|
||||||
|
|
||||||
PATTERN_ESCAPES = [
|
logger = logging.getLogger("pycalver.v2patterns")
|
||||||
("\u005c", "\u005c\u005c"),
|
|
||||||
("-" , "\u005c-"),
|
# NOTE (mb 2020-09-17): For patterns with different options '(AAA|BB|C)', the
|
||||||
("." , "\u005c."),
|
# patterns with more digits should be first/left of those with fewer digits:
|
||||||
("+" , "\u005c+"),
|
#
|
||||||
("*" , "\u005c*"),
|
# good: (?:1[0-2]|[1-9])
|
||||||
("?" , "\u005c?"),
|
# bad: (?:[1-9]|1[0-2])
|
||||||
("{" , "\u005c{"),
|
#
|
||||||
("}" , "\u005c}"),
|
# This ensures that the longest match is done for a pattern.
|
||||||
# ("[" , "\u005c["), # [braces] are used for optional parts
|
#
|
||||||
# ("]" , "\u005c]"),
|
# This implies that patterns for smaller numbers sometimes must be right of
|
||||||
("(", "\u005c("),
|
# those for larger numbers. To be consistent we use this ordering not
|
||||||
(")", "\u005c)"),
|
# sometimes but always (even though in theory it wouldn't matter):
|
||||||
]
|
#
|
||||||
|
# good: (?:3[0-1]|[1-2][0-9]|[1-9])
|
||||||
|
# bad: (?:[1-2][0-9]|3[0-1]|[1-9])
|
||||||
|
|
||||||
# NOTE (mb 2020-09-17): For patterns with different options, the longer
|
|
||||||
# patterns should be first/left (e.g. for 'MM', `1[0-2]` before `[1-9]`).
|
|
||||||
# This ensures that the longest match is done rather than the shortest.
|
|
||||||
# To have a consistent ordering, we always put the pattern that matches
|
|
||||||
# the larger number first (even if the patterns would otherwise be the
|
|
||||||
# same size).
|
|
||||||
|
|
||||||
PART_PATTERNS = {
|
PART_PATTERNS = {
|
||||||
# Based on calver.org
|
# Based on calver.org
|
||||||
|
|
@ -239,14 +237,28 @@ def _replace_pattern_parts(pattern: str) -> str:
|
||||||
return result_pattern
|
return result_pattern
|
||||||
|
|
||||||
|
|
||||||
def compile_pattern_str(pattern: str) -> str:
|
def _compile_pattern_re(version_pattern: str, raw_pattern: str) -> typ.Pattern[str]:
|
||||||
for char, escaped in PATTERN_ESCAPES:
|
escaped_pattern = raw_pattern
|
||||||
pattern = pattern.replace(char, escaped)
|
for char, escaped in RE_PATTERN_ESCAPES:
|
||||||
|
# [] braces are used for optional parts, such as [-TAG]/[-beta]
|
||||||
|
is_semantic_char = char in "[]"
|
||||||
|
if not is_semantic_char:
|
||||||
|
# escape it so it is a literal in the re pattern
|
||||||
|
escaped_pattern = escaped_pattern.replace(char, escaped)
|
||||||
|
|
||||||
return _replace_pattern_parts(pattern)
|
escaped_pattern = raw_pattern.replace("[", "\u005c[").replace("]", "\u005c]")
|
||||||
|
normalized_pattern = escaped_pattern.replace("{version}", version_pattern)
|
||||||
|
print(">>>>", (raw_pattern ,))
|
||||||
|
print("....", (escaped_pattern ,))
|
||||||
|
print("....", (normalized_pattern,))
|
||||||
|
print("<<<<", (normalized_pattern,))
|
||||||
|
|
||||||
|
# TODO (mb 2020-09-19): replace {version} etc with version_pattern
|
||||||
|
pattern_str = _replace_pattern_parts(escaped_pattern)
|
||||||
|
return re.compile(pattern_str)
|
||||||
|
|
||||||
|
|
||||||
def compile_pattern(pattern: str) -> v1patterns.Pattern:
|
def compile_pattern(version_pattern: str, raw_pattern: typ.Optional[str] = None) -> Pattern:
|
||||||
pattern_str = compile_pattern_str(pattern)
|
_raw_pattern = version_pattern if raw_pattern is None else raw_pattern
|
||||||
pattern_re = re.compile(pattern_str)
|
regexp = _compile_pattern_re(version_pattern, _raw_pattern)
|
||||||
return v1patterns.Pattern(pattern, pattern_re)
|
return Pattern(version_pattern, _raw_pattern, regexp)
|
||||||
198
src/pycalver/v2rewrite.py
Normal file
198
src/pycalver/v2rewrite.py
Normal file
|
|
@ -0,0 +1,198 @@
|
||||||
|
# This file is part of the pycalver project
|
||||||
|
# https://github.com/mbarkhau/pycalver
|
||||||
|
#
|
||||||
|
# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
"""Rewrite files, updating occurences of version strings."""
|
||||||
|
|
||||||
|
import io
|
||||||
|
import typing as typ
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from . import parse
|
||||||
|
from . import config
|
||||||
|
from . import rewrite
|
||||||
|
from . import version
|
||||||
|
from . import v2version
|
||||||
|
from .patterns import Pattern
|
||||||
|
|
||||||
|
logger = logging.getLogger("pycalver.v2rewrite")
|
||||||
|
|
||||||
|
|
||||||
|
def rewrite_lines(
|
||||||
|
patterns : typ.List[Pattern],
|
||||||
|
new_vinfo: version.V2VersionInfo,
|
||||||
|
old_lines: typ.List[str],
|
||||||
|
) -> typ.List[str]:
|
||||||
|
"""Replace occurances of patterns in old_lines with new_vinfo.
|
||||||
|
|
||||||
|
>>> from .v2patterns import compile_pattern
|
||||||
|
>>> version_pattern = "vYYYY0M.BUILD[-TAG]"
|
||||||
|
>>> new_vinfo = v2version.parse_version_info("v201811.0123-beta", version_pattern)
|
||||||
|
>>> patterns = [compile_pattern(version_pattern, '__version__ = "{version}"')]
|
||||||
|
>>> rewrite_lines(patterns, new_vinfo, ['__version__ = "v201809.0002-alpha" '])
|
||||||
|
['__version__ = "v201811.0123-beta" ']
|
||||||
|
|
||||||
|
>>> rewrite_lines(patterns, new_vinfo, ['__version__ = "v201809.0002-alpha" # comment'])
|
||||||
|
['__version__ = "v201811.0123-beta" # comment']
|
||||||
|
|
||||||
|
>>> patterns = [compile_pattern(version_pattern, '__version__ = "{pep440_version}"')]
|
||||||
|
>>> old_lines = ['__version__ = "201809.2a0"']
|
||||||
|
>>> rewrite_lines(patterns, new_vinfo, old_lines)
|
||||||
|
['__version__ = "201811.123b0"']
|
||||||
|
"""
|
||||||
|
new_lines = old_lines[:]
|
||||||
|
found_patterns = set()
|
||||||
|
|
||||||
|
for match in parse.iter_matches(old_lines, patterns):
|
||||||
|
found_patterns.add(match.pattern.raw_pattern)
|
||||||
|
replacement = v2version.format_version(new_vinfo, match.pattern.raw_pattern)
|
||||||
|
span_l, span_r = match.span
|
||||||
|
new_line = match.line[:span_l] + replacement + match.line[span_r:]
|
||||||
|
new_lines[match.lineno] = new_line
|
||||||
|
|
||||||
|
non_matched_patterns = set(patterns) - found_patterns
|
||||||
|
if non_matched_patterns:
|
||||||
|
for nmp in non_matched_patterns:
|
||||||
|
logger.error(f"No match for pattern '{nmp.raw_pattern}'")
|
||||||
|
logger.error(f"Pattern compiles to regex '{nmp.regexp.pattern}'")
|
||||||
|
raise rewrite.NoPatternMatch("Invalid pattern(s)")
|
||||||
|
else:
|
||||||
|
return new_lines
|
||||||
|
|
||||||
|
|
||||||
|
def rfd_from_content(
|
||||||
|
patterns : typ.List[Pattern],
|
||||||
|
new_vinfo: version.V2VersionInfo,
|
||||||
|
content : str,
|
||||||
|
path : str = "<path>",
|
||||||
|
) -> rewrite.RewrittenFileData:
|
||||||
|
r"""Rewrite pattern occurrences with version string.
|
||||||
|
|
||||||
|
>>> version_pattern = "vYYYY0M.BUILD[-TAG]"
|
||||||
|
>>> new_vinfo = v2version.parse_version_info("v201809.0123", version_pattern)
|
||||||
|
>>> raw_patterns = ['__version__ = "vYYYY0M.BUILD[-TAG]"']
|
||||||
|
>>> patterns =
|
||||||
|
>>> content = '__version__ = "v201809.0001-alpha"'
|
||||||
|
>>> rfd = rfd_from_content(patterns, new_vinfo, content)
|
||||||
|
>>> rfd.new_lines
|
||||||
|
['__version__ = "v201809.0123"']
|
||||||
|
|
||||||
|
>>> version_pattern = "vMAJOR.MINOR.PATCH"
|
||||||
|
>>> new_vinfo = v2version.parse_version_info("v1.2.3", version_pattern)
|
||||||
|
>>> raw_patterns = ['__version__ = "vMAJOR.MINOR.PATCH"']
|
||||||
|
>>> patterns =
|
||||||
|
>>> content = '__version__ = "v1.2.2"'
|
||||||
|
>>> rfd = rfd_from_content(patterns, new_vinfo, content)
|
||||||
|
>>> rfd.new_lines
|
||||||
|
['__version__ = "v1.2.3"']
|
||||||
|
"""
|
||||||
|
line_sep = rewrite.detect_line_sep(content)
|
||||||
|
old_lines = content.split(line_sep)
|
||||||
|
new_lines = rewrite_lines(patterns, new_vinfo, old_lines)
|
||||||
|
return rewrite.RewrittenFileData(path, line_sep, old_lines, new_lines)
|
||||||
|
|
||||||
|
|
||||||
|
def iter_rewritten(
|
||||||
|
file_patterns: config.PatternsByFile,
|
||||||
|
new_vinfo : version.V2VersionInfo,
|
||||||
|
) -> typ.Iterable[rewrite.RewrittenFileData]:
|
||||||
|
r'''Iterate over files with version string replaced.
|
||||||
|
|
||||||
|
>>> version_pattern = "vYYYY0M.BUILD[-TAG]"
|
||||||
|
>>> file_patterns = {"src/pycalver/__init__.py": ['__version__ = "vYYYY0M.BUILD[-TAG]"']}
|
||||||
|
>>> new_vinfo = v2version.parse_version_info("v201809.0123", version_pattern)
|
||||||
|
>>> rewritten_datas = iter_rewritten(file_patterns, new_vinfo)
|
||||||
|
>>> rfd = list(rewritten_datas)[0]
|
||||||
|
>>> expected = [
|
||||||
|
... '# This file is part of the pycalver project',
|
||||||
|
... '# https://github.com/mbarkhau/pycalver',
|
||||||
|
... '#',
|
||||||
|
... '# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License',
|
||||||
|
... '# SPDX-License-Identifier: MIT',
|
||||||
|
... '"""PyCalVer: CalVer for Python Packages."""',
|
||||||
|
... '',
|
||||||
|
... '__version__ = "v201809.0123"',
|
||||||
|
... '',
|
||||||
|
... ]
|
||||||
|
>>> assert rfd.new_lines == expected
|
||||||
|
'''
|
||||||
|
|
||||||
|
fobj: typ.IO[str]
|
||||||
|
|
||||||
|
for file_path, patterns in rewrite.iter_path_patterns_items(file_patterns):
|
||||||
|
with file_path.open(mode="rt", encoding="utf-8") as fobj:
|
||||||
|
content = fobj.read()
|
||||||
|
|
||||||
|
rfd = rfd_from_content(patterns, new_vinfo, content)
|
||||||
|
yield rfd._replace(path=str(file_path))
|
||||||
|
|
||||||
|
|
||||||
|
def diff(
|
||||||
|
old_vinfo : version.V2VersionInfo,
|
||||||
|
new_vinfo : version.V2VersionInfo,
|
||||||
|
file_patterns: config.PatternsByFile,
|
||||||
|
) -> str:
|
||||||
|
r"""Generate diffs of rewritten files.
|
||||||
|
|
||||||
|
>>> old_vinfo = v2version.parse_version_info("v201809.0123", version_pattern)
|
||||||
|
>>> new_vinfo = v2version.parse_version_info("v201810.1124", version_pattern)
|
||||||
|
>>> file_patterns = {"src/pycalver/__init__.py": ['__version__ = "vYYYY0M.BUILD[-TAG]"']}
|
||||||
|
>>> diff_str = diff(old_vinfo, new_vinfo, file_patterns)
|
||||||
|
>>> lines = diff_str.split("\n")
|
||||||
|
>>> lines[:2]
|
||||||
|
['--- src/pycalver/__init__.py', '+++ src/pycalver/__init__.py']
|
||||||
|
>>> assert lines[6].startswith('-__version__ = "v2')
|
||||||
|
>>> assert not lines[6].startswith('-__version__ = "v201810.1124"')
|
||||||
|
>>> lines[7]
|
||||||
|
'+__version__ = "v201810.1124"'
|
||||||
|
|
||||||
|
>>> file_patterns = {"LICENSE": ['Copyright (c) 2018-YYYY']}
|
||||||
|
>>> diff_str = diff(old_vinfo, new_vinfo, file_patterns)
|
||||||
|
>>> assert not diff_str
|
||||||
|
"""
|
||||||
|
|
||||||
|
full_diff = ""
|
||||||
|
fobj: typ.IO[str]
|
||||||
|
|
||||||
|
for file_path, patterns in sorted(rewrite.iter_path_patterns_items(file_patterns)):
|
||||||
|
with file_path.open(mode="rt", encoding="utf-8") as fobj:
|
||||||
|
content = fobj.read()
|
||||||
|
|
||||||
|
patterns_with_change = 0
|
||||||
|
for pattern in patterns:
|
||||||
|
old_str = v2version.format_version(old_vinfo, pattern.raw_pattern)
|
||||||
|
new_str = v2version.format_version(new_vinfo, pattern.raw_pattern)
|
||||||
|
if old_str != new_str:
|
||||||
|
patterns_with_change += 1
|
||||||
|
|
||||||
|
try:
|
||||||
|
rfd = rfd_from_content(patterns, new_vinfo, content)
|
||||||
|
except rewrite.NoPatternMatch:
|
||||||
|
# pylint:disable=raise-missing-from ; we support py2, so not an option
|
||||||
|
errmsg = f"No patterns matched for '{file_path}'"
|
||||||
|
raise rewrite.NoPatternMatch(errmsg)
|
||||||
|
|
||||||
|
rfd = rfd._replace(path=str(file_path))
|
||||||
|
lines = rewrite.diff_lines(rfd)
|
||||||
|
if len(lines) == 0 and patterns_with_change > 0:
|
||||||
|
errmsg = f"No patterns matched for '{file_path}'"
|
||||||
|
raise rewrite.NoPatternMatch(errmsg)
|
||||||
|
|
||||||
|
full_diff += "\n".join(lines) + "\n"
|
||||||
|
|
||||||
|
full_diff = full_diff.rstrip("\n")
|
||||||
|
return full_diff
|
||||||
|
|
||||||
|
|
||||||
|
def rewrite_files(
|
||||||
|
file_patterns: config.PatternsByFile,
|
||||||
|
new_vinfo : version.V2VersionInfo,
|
||||||
|
) -> None:
|
||||||
|
"""Rewrite project files, updating each with the new version."""
|
||||||
|
fobj: typ.IO[str]
|
||||||
|
|
||||||
|
for file_data in iter_rewritten(file_patterns, new_vinfo):
|
||||||
|
new_content = file_data.line_sep.join(file_data.new_lines)
|
||||||
|
with io.open(file_data.path, mode="wt", encoding="utf-8") as fobj:
|
||||||
|
fobj.write(new_content)
|
||||||
|
|
@ -9,102 +9,28 @@ import typing as typ
|
||||||
import logging
|
import logging
|
||||||
import datetime as dt
|
import datetime as dt
|
||||||
|
|
||||||
import lexid
|
from . import version
|
||||||
|
from . import v2patterns
|
||||||
|
|
||||||
import pycalver2.patterns as v2patterns
|
logger = logging.getLogger("pycalver.v2version")
|
||||||
|
|
||||||
# import pycalver.version as v1version
|
|
||||||
# import pycalver.patterns as v1patterns
|
|
||||||
|
|
||||||
logger = logging.getLogger("pycalver.version")
|
|
||||||
|
|
||||||
|
|
||||||
# The test suite may replace this.
|
CalInfo = typ.Union[version.V2CalendarInfo, version.V2VersionInfo]
|
||||||
TODAY = dt.datetime.utcnow().date()
|
|
||||||
|
|
||||||
|
|
||||||
ZERO_VALUES = {
|
def _is_later_than(old: CalInfo, new: CalInfo) -> bool:
|
||||||
'MAJOR': "0",
|
"""Is old > new based on non None fields."""
|
||||||
'MINOR': "0",
|
for field in version.V1CalendarInfo._fields:
|
||||||
'PATCH': "0",
|
aval = getattr(old, field)
|
||||||
'TAG' : "final",
|
bval = getattr(new, field)
|
||||||
'PYTAG': "",
|
if not (aval is None or bval is None):
|
||||||
'NUM' : "0",
|
if aval > bval:
|
||||||
}
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
TAG_BY_PEP440_TAG = {
|
def _ver_to_cal_info(vinfo: version.V2VersionInfo) -> version.V2CalendarInfo:
|
||||||
'a' : 'alpha',
|
return version.V2CalendarInfo(
|
||||||
'b' : 'beta',
|
|
||||||
"" : 'final',
|
|
||||||
'rc' : 'rc',
|
|
||||||
'dev' : 'dev',
|
|
||||||
'post': 'post',
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
PEP440_TAG_BY_TAG = {
|
|
||||||
'alpha': "a",
|
|
||||||
'beta' : "b",
|
|
||||||
'final': "",
|
|
||||||
'pre' : "rc",
|
|
||||||
'rc' : "rc",
|
|
||||||
'dev' : "dev",
|
|
||||||
'post' : "post",
|
|
||||||
}
|
|
||||||
|
|
||||||
assert set(TAG_BY_PEP440_TAG.keys()) == set(PEP440_TAG_BY_TAG.values())
|
|
||||||
assert set(TAG_BY_PEP440_TAG.values()) < set(PEP440_TAG_BY_TAG.keys())
|
|
||||||
|
|
||||||
# PEP440_TAGS_REVERSE = {
|
|
||||||
# "a" : 'alpha',
|
|
||||||
# "b" : 'beta',
|
|
||||||
# "rc" : 'rc',
|
|
||||||
# "dev" : 'dev',
|
|
||||||
# "post": 'post',
|
|
||||||
# }
|
|
||||||
|
|
||||||
|
|
||||||
MaybeInt = typ.Optional[int]
|
|
||||||
|
|
||||||
|
|
||||||
class CalendarInfo(typ.NamedTuple):
|
|
||||||
"""Container for calendar components of version strings."""
|
|
||||||
|
|
||||||
year_y : MaybeInt
|
|
||||||
year_g : MaybeInt
|
|
||||||
quarter: MaybeInt
|
|
||||||
month : MaybeInt
|
|
||||||
dom : MaybeInt
|
|
||||||
doy : MaybeInt
|
|
||||||
week_w : MaybeInt
|
|
||||||
week_u : MaybeInt
|
|
||||||
week_v : MaybeInt
|
|
||||||
|
|
||||||
|
|
||||||
class VersionInfo(typ.NamedTuple):
|
|
||||||
"""Container for parsed version string."""
|
|
||||||
|
|
||||||
year_y : MaybeInt
|
|
||||||
year_g : MaybeInt
|
|
||||||
quarter: MaybeInt
|
|
||||||
month : MaybeInt
|
|
||||||
dom : MaybeInt
|
|
||||||
doy : MaybeInt
|
|
||||||
week_w : MaybeInt
|
|
||||||
week_u : MaybeInt
|
|
||||||
week_v : MaybeInt
|
|
||||||
major : int
|
|
||||||
minor : int
|
|
||||||
patch : int
|
|
||||||
num : int
|
|
||||||
bid : str
|
|
||||||
tag : str
|
|
||||||
pytag : str
|
|
||||||
|
|
||||||
|
|
||||||
def _ver_to_cal_info(vinfo: VersionInfo) -> CalendarInfo:
|
|
||||||
return CalendarInfo(
|
|
||||||
vinfo.year_y,
|
vinfo.year_y,
|
||||||
vinfo.year_g,
|
vinfo.year_g,
|
||||||
vinfo.quarter,
|
vinfo.quarter,
|
||||||
|
|
@ -117,32 +43,7 @@ def _ver_to_cal_info(vinfo: VersionInfo) -> CalendarInfo:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _date_from_doy(year: int, doy: int) -> dt.date:
|
def cal_info(date: dt.date = None) -> version.V2CalendarInfo:
|
||||||
"""Parse date from year and day of year (1 indexed).
|
|
||||||
|
|
||||||
>>> cases = [
|
|
||||||
... (2016, 1), (2016, 31), (2016, 31 + 1), (2016, 31 + 29), (2016, 31 + 30),
|
|
||||||
... (2017, 1), (2017, 31), (2017, 31 + 1), (2017, 31 + 28), (2017, 31 + 29),
|
|
||||||
... ]
|
|
||||||
>>> dates = [_date_from_doy(year, month) for year, month in cases]
|
|
||||||
>>> assert [(d.month, d.day) for d in dates] == [
|
|
||||||
... (1, 1), (1, 31), (2, 1), (2, 29), (3, 1),
|
|
||||||
... (1, 1), (1, 31), (2, 1), (2, 28), (3, 1),
|
|
||||||
... ]
|
|
||||||
"""
|
|
||||||
return dt.date(year, 1, 1) + dt.timedelta(days=doy - 1)
|
|
||||||
|
|
||||||
|
|
||||||
def _quarter_from_month(month: int) -> int:
|
|
||||||
"""Calculate quarter (1 indexed) from month (1 indexed).
|
|
||||||
|
|
||||||
>>> [_quarter_from_month(month) for month in range(1, 13)]
|
|
||||||
[1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4]
|
|
||||||
"""
|
|
||||||
return ((month - 1) // 3) + 1
|
|
||||||
|
|
||||||
|
|
||||||
def cal_info(date: dt.date = None) -> CalendarInfo:
|
|
||||||
"""Generate calendar components for current date.
|
"""Generate calendar components for current date.
|
||||||
|
|
||||||
>>> import datetime as dt
|
>>> import datetime as dt
|
||||||
|
|
@ -164,12 +65,12 @@ def cal_info(date: dt.date = None) -> CalendarInfo:
|
||||||
(2019, 2, 4, 7, 97, 13, 14, 14)
|
(2019, 2, 4, 7, 97, 13, 14, 14)
|
||||||
"""
|
"""
|
||||||
if date is None:
|
if date is None:
|
||||||
date = TODAY
|
date = version.TODAY
|
||||||
|
|
||||||
kwargs = {
|
kwargs = {
|
||||||
'year_y' : date.year,
|
'year_y' : date.year,
|
||||||
'year_g' : int(date.strftime("%G"), base=10),
|
'year_g' : int(date.strftime("%G"), base=10),
|
||||||
'quarter': _quarter_from_month(date.month),
|
'quarter': version.quarter_from_month(date.month),
|
||||||
'month' : date.month,
|
'month' : date.month,
|
||||||
'dom' : date.day,
|
'dom' : date.day,
|
||||||
'doy' : int(date.strftime("%j"), base=10),
|
'doy' : int(date.strftime("%j"), base=10),
|
||||||
|
|
@ -178,10 +79,12 @@ def cal_info(date: dt.date = None) -> CalendarInfo:
|
||||||
'week_v' : int(date.strftime("%V"), base=10),
|
'week_v' : int(date.strftime("%V"), base=10),
|
||||||
}
|
}
|
||||||
|
|
||||||
return CalendarInfo(**kwargs)
|
return version.V2CalendarInfo(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
VALID_FIELD_KEYS = set(VersionInfo._fields) | {'version'}
|
VALID_FIELD_KEYS = set(version.V2VersionInfo._fields) | {'version'}
|
||||||
|
|
||||||
|
MaybeInt = typ.Optional[int]
|
||||||
|
|
||||||
FieldKey = str
|
FieldKey = str
|
||||||
MatchGroupKey = str
|
MatchGroupKey = str
|
||||||
|
|
@ -190,44 +93,46 @@ MatchGroupStr = str
|
||||||
PatternGroups = typ.Dict[FieldKey, MatchGroupStr]
|
PatternGroups = typ.Dict[FieldKey, MatchGroupStr]
|
||||||
FieldValues = typ.Dict[FieldKey, MatchGroupStr]
|
FieldValues = typ.Dict[FieldKey, MatchGroupStr]
|
||||||
|
|
||||||
|
VersionInfoKW = typ.Dict[str, typ.Union[str, int, None]]
|
||||||
|
|
||||||
def _parse_version_info(field_values: FieldValues) -> VersionInfo:
|
|
||||||
"""Parse normalized VersionInfo from groups of a matched pattern.
|
|
||||||
|
|
||||||
>>> vnfo = _parse_version_info({'year_y': "2018", 'month': "11", 'bid': "0099"})
|
def _parse_version_info(field_values: FieldValues) -> version.V2VersionInfo:
|
||||||
>>> (vnfo.year_y, vnfo.month, vnfo.quarter, vnfo.bid, vnfo.tag)
|
"""Parse normalized V2VersionInfo from groups of a matched pattern.
|
||||||
|
|
||||||
|
>>> vinfo = _parse_version_info({'year_y': "2018", 'month': "11", 'bid': "0099"})
|
||||||
|
>>> (vinfo.year_y, vinfo.month, vinfo.quarter, vinfo.bid, vinfo.tag)
|
||||||
(2018, 11, 4, '0099', 'final')
|
(2018, 11, 4, '0099', 'final')
|
||||||
|
|
||||||
>>> vnfo = _parse_version_info({'year_y': "2018", 'doy': "11", 'bid': "099", 'tag': "beta"})
|
>>> vinfo = _parse_version_info({'year_y': "2018", 'doy': "11", 'bid': "099", 'tag': "beta"})
|
||||||
>>> (vnfo.year_y, vnfo.month, vnfo.dom, vnfo.doy, vnfo.bid, vnfo.tag)
|
>>> (vinfo.year_y, vinfo.month, vinfo.dom, vinfo.doy, vinfo.bid, vinfo.tag)
|
||||||
(2018, 1, 11, 11, '099', 'beta')
|
(2018, 1, 11, 11, '099', 'beta')
|
||||||
|
|
||||||
>>> vnfo = _parse_version_info({'year_y': "2018", 'month': "6", 'dom': "15"})
|
>>> vinfo = _parse_version_info({'year_y': "2018", 'month': "6", 'dom': "15"})
|
||||||
>>> (vnfo.year_y, vnfo.month, vnfo.dom, vnfo.doy)
|
>>> (vinfo.year_y, vinfo.month, vinfo.dom, vinfo.doy)
|
||||||
(2018, 6, 15, 166)
|
(2018, 6, 15, 166)
|
||||||
|
|
||||||
>>> vnfo = _parse_version_info({'major': "1", 'minor': "23", 'patch': "45"})
|
>>> vinfo = _parse_version_info({'major': "1", 'minor': "23", 'patch': "45"})
|
||||||
>>> (vnfo.major, vnfo.minor, vnfo.patch)
|
>>> (vinfo.major, vinfo.minor, vinfo.patch)
|
||||||
(1, 23, 45)
|
(1, 23, 45)
|
||||||
|
|
||||||
>>> vnfo = _parse_version_info({'major': "1", 'minor': "023", 'patch': "0045"})
|
>>> vinfo = _parse_version_info({'major': "1", 'minor': "023", 'patch': "0045"})
|
||||||
>>> (vnfo.major, vnfo.minor, vnfo.patch)
|
>>> (vinfo.major, vinfo.minor, vinfo.patch)
|
||||||
(1, 23, 45)
|
(1, 23, 45)
|
||||||
|
|
||||||
>>> vnfo = _parse_version_info({'year_y': "2021", 'week_w': "02"})
|
>>> vinfo = _parse_version_info({'year_y': "2021", 'week_w': "02"})
|
||||||
>>> (vnfo.year_y, vnfo.week_w)
|
>>> (vinfo.year_y, vinfo.week_w)
|
||||||
(2021, 2)
|
(2021, 2)
|
||||||
>>> vnfo = _parse_version_info({'year_y': "2021", 'week_u': "02"})
|
>>> vinfo = _parse_version_info({'year_y': "2021", 'week_u': "02"})
|
||||||
>>> (vnfo.year_y, vnfo.week_u)
|
>>> (vinfo.year_y, vinfo.week_u)
|
||||||
(2021, 2)
|
(2021, 2)
|
||||||
>>> vnfo = _parse_version_info({'year_g': "2021", 'week_v': "02"})
|
>>> vinfo = _parse_version_info({'year_g': "2021", 'week_v': "02"})
|
||||||
>>> (vnfo.year_g, vnfo.week_v)
|
>>> (vinfo.year_g, vinfo.week_v)
|
||||||
(2021, 2)
|
(2021, 2)
|
||||||
|
|
||||||
>>> vnfo = _parse_version_info({'year_y': "2021", 'month': "01", 'dom': "03"})
|
>>> vinfo = _parse_version_info({'year_y': "2021", 'month': "01", 'dom': "03"})
|
||||||
>>> (vnfo.year_y, vnfo.month, vnfo.dom, vnfo.tag)
|
>>> (vinfo.year_y, vinfo.month, vinfo.dom, vinfo.tag)
|
||||||
(2021, 1, 3, 'final')
|
(2021, 1, 3, 'final')
|
||||||
>>> (vnfo.year_y, vnfo.week_w, vnfo.year_y, vnfo.week_u,vnfo.year_g, vnfo.week_v)
|
>>> (vinfo.year_y, vinfo.week_w, vinfo.year_y, vinfo.week_u,vinfo.year_g, vinfo.week_v)
|
||||||
(2021, 0, 2021, 1, 2020, 53)
|
(2021, 0, 2021, 1, 2020, 53)
|
||||||
"""
|
"""
|
||||||
for key in field_values:
|
for key in field_values:
|
||||||
|
|
@ -238,9 +143,9 @@ def _parse_version_info(field_values: FieldValues) -> VersionInfo:
|
||||||
pytag = fvals.get('pytag') or ""
|
pytag = fvals.get('pytag') or ""
|
||||||
|
|
||||||
if tag and not pytag:
|
if tag and not pytag:
|
||||||
pytag = PEP440_TAG_BY_TAG[tag]
|
pytag = version.PEP440_TAG_BY_TAG[tag]
|
||||||
elif pytag and not tag:
|
elif pytag and not tag:
|
||||||
tag = TAG_BY_PEP440_TAG[pytag]
|
tag = version.TAG_BY_PEP440_TAG[pytag]
|
||||||
|
|
||||||
date: typ.Optional[dt.date] = None
|
date: typ.Optional[dt.date] = None
|
||||||
|
|
||||||
|
|
@ -256,7 +161,7 @@ def _parse_version_info(field_values: FieldValues) -> VersionInfo:
|
||||||
week_v: MaybeInt = int(fvals['week_v']) if 'week_v' in fvals else None
|
week_v: MaybeInt = int(fvals['week_v']) if 'week_v' in fvals else None
|
||||||
|
|
||||||
if year_y and doy:
|
if year_y and doy:
|
||||||
date = _date_from_doy(year_y, doy)
|
date = version.date_from_doy(year_y, doy)
|
||||||
month = date.month
|
month = date.month
|
||||||
dom = date.day
|
dom = date.day
|
||||||
else:
|
else:
|
||||||
|
|
@ -279,7 +184,7 @@ def _parse_version_info(field_values: FieldValues) -> VersionInfo:
|
||||||
|
|
||||||
quarter = int(fvals['quarter']) if 'quarter' in fvals else None
|
quarter = int(fvals['quarter']) if 'quarter' in fvals else None
|
||||||
if quarter is None and month:
|
if quarter is None and month:
|
||||||
quarter = _quarter_from_month(month)
|
quarter = version.quarter_from_month(month)
|
||||||
|
|
||||||
# NOTE (mb 2020-09-18): If a part is optional, fvals[<field>] may be None
|
# NOTE (mb 2020-09-18): If a part is optional, fvals[<field>] may be None
|
||||||
major = int(fvals.get('major') or 0)
|
major = int(fvals.get('major') or 0)
|
||||||
|
|
@ -288,7 +193,7 @@ def _parse_version_info(field_values: FieldValues) -> VersionInfo:
|
||||||
num = int(fvals.get('num' ) or 0)
|
num = int(fvals.get('num' ) or 0)
|
||||||
bid = fvals['bid'] if 'bid' in fvals else "1000"
|
bid = fvals['bid'] if 'bid' in fvals else "1000"
|
||||||
|
|
||||||
vnfo = VersionInfo(
|
vinfo = version.V2VersionInfo(
|
||||||
year_y=year_y,
|
year_y=year_y,
|
||||||
year_g=year_g,
|
year_g=year_g,
|
||||||
quarter=quarter,
|
quarter=quarter,
|
||||||
|
|
@ -306,74 +211,69 @@ def _parse_version_info(field_values: FieldValues) -> VersionInfo:
|
||||||
tag=tag,
|
tag=tag,
|
||||||
pytag=pytag,
|
pytag=pytag,
|
||||||
)
|
)
|
||||||
return vnfo
|
return vinfo
|
||||||
|
|
||||||
|
|
||||||
VersionInfoKW = typ.Dict[str, typ.Union[str, int, None]]
|
def parse_version_info(
|
||||||
|
version_str: str, raw_pattern: str = "vYYYY0M.BUILD[-TAG[NUM]]"
|
||||||
|
) -> version.V2VersionInfo:
|
||||||
|
"""Parse normalized V2VersionInfo.
|
||||||
|
|
||||||
|
>>> vinfo = parse_version_info("v201712.0033-beta0", raw_pattern="vYYYY0M.BUILD[-TAG[NUM]]")
|
||||||
class PatternError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def parse_version_info(version_str: str, pattern: str = "vYYYY0M.BUILD[-TAG[NUM]]") -> VersionInfo:
|
|
||||||
"""Parse normalized VersionInfo.
|
|
||||||
|
|
||||||
>>> vnfo = parse_version_info("v201712.0033-beta0", pattern="vYYYY0M.BUILD[-TAG[NUM]]")
|
|
||||||
>>> fvals = {'year_y': 2017, 'month': 12, 'bid': "0033", 'tag': "beta", 'num': 0}
|
>>> fvals = {'year_y': 2017, 'month': 12, 'bid': "0033", 'tag': "beta", 'num': 0}
|
||||||
>>> assert vnfo == _parse_version_info(fvals)
|
>>> assert vinfo == _parse_version_info(fvals)
|
||||||
|
|
||||||
>>> vnfo = parse_version_info("v201712.0033-beta", pattern="vYYYY0M.BUILD[-TAG[NUM]]")
|
>>> vinfo = parse_version_info("v201712.0033-beta", raw_pattern="vYYYY0M.BUILD[-TAG[NUM]]")
|
||||||
>>> fvals = {'year_y': 2017, 'month': 12, 'bid': "0033", 'tag': "beta"}
|
>>> fvals = {'year_y': 2017, 'month': 12, 'bid': "0033", 'tag': "beta"}
|
||||||
>>> assert vnfo == _parse_version_info(fvals)
|
>>> assert vinfo == _parse_version_info(fvals)
|
||||||
|
|
||||||
>>> vnfo = parse_version_info("v201712.0033", pattern="vYYYY0M.BUILD[-TAG[NUM]]")
|
>>> vinfo = parse_version_info("v201712.0033", raw_pattern="vYYYY0M.BUILD[-TAG[NUM]]")
|
||||||
>>> fvals = {'year_y': 2017, 'month': 12, 'bid': "0033"}
|
>>> fvals = {'year_y': 2017, 'month': 12, 'bid': "0033"}
|
||||||
>>> assert vnfo == _parse_version_info(fvals)
|
>>> assert vinfo == _parse_version_info(fvals)
|
||||||
|
|
||||||
>>> vnfo = parse_version_info("1.23.456", pattern="MAJOR.MINOR.PATCH")
|
>>> vinfo = parse_version_info("1.23.456", raw_pattern="MAJOR.MINOR.PATCH")
|
||||||
>>> fvals = {'major': "1", 'minor': "23", 'patch': "456"}
|
>>> fvals = {'major': "1", 'minor': "23", 'patch': "456"}
|
||||||
>>> assert vnfo == _parse_version_info(fvals)
|
>>> assert vinfo == _parse_version_info(fvals)
|
||||||
"""
|
"""
|
||||||
pattern_tup = v2patterns.compile_pattern(pattern)
|
pattern = v2patterns.compile_pattern(raw_pattern)
|
||||||
match = pattern_tup.regexp.match(version_str)
|
match = pattern.regexp.match(version_str)
|
||||||
if match is None:
|
if match is None:
|
||||||
err_msg = (
|
err_msg = (
|
||||||
f"Invalid version string '{version_str}' "
|
f"Invalid version string '{version_str}' "
|
||||||
f"for pattern '{pattern}'/'{pattern_tup.regexp.pattern}'"
|
f"for pattern '{raw_pattern}'/'{pattern.regexp.pattern}'"
|
||||||
)
|
)
|
||||||
raise PatternError(err_msg)
|
raise version.PatternError(err_msg)
|
||||||
else:
|
else:
|
||||||
field_values = match.groupdict()
|
field_values = match.groupdict()
|
||||||
return _parse_version_info(field_values)
|
return _parse_version_info(field_values)
|
||||||
|
|
||||||
|
|
||||||
def is_valid(version_str: str, pattern: str = "vYYYY0M.BUILD[-TAG]") -> bool:
|
def is_valid(version_str: str, raw_pattern: str = "vYYYY0M.BUILD[-TAG]") -> bool:
|
||||||
"""Check if a version matches a pattern.
|
"""Check if a version matches a pattern.
|
||||||
|
|
||||||
>>> is_valid("v201712.0033-beta", pattern="vYYYY0M.BUILD[-TAG]")
|
>>> is_valid("v201712.0033-beta", raw_pattern="vYYYY0M.BUILD[-TAG]")
|
||||||
True
|
True
|
||||||
>>> is_valid("v201712.0033-beta", pattern="MAJOR.MINOR.PATCH")
|
>>> is_valid("v201712.0033-beta", raw_pattern="MAJOR.MINOR.PATCH")
|
||||||
False
|
False
|
||||||
>>> is_valid("1.2.3", pattern="MAJOR.MINOR.PATCH")
|
>>> is_valid("1.2.3", raw_pattern="MAJOR.MINOR.PATCH")
|
||||||
True
|
True
|
||||||
>>> is_valid("v201712.0033-beta", pattern="MAJOR.MINOR.PATCH")
|
>>> is_valid("v201712.0033-beta", raw_pattern="MAJOR.MINOR.PATCH")
|
||||||
False
|
False
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
parse_version_info(version_str, pattern)
|
parse_version_info(version_str, raw_pattern)
|
||||||
return True
|
return True
|
||||||
except PatternError:
|
except version.PatternError:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
TemplateKwargs = typ.Dict[str, typ.Union[str, int, None]]
|
TemplateKwargs = typ.Dict[str, typ.Union[str, int, None]]
|
||||||
|
|
||||||
|
|
||||||
def _format_part_values(vinfo: VersionInfo) -> typ.Dict[str, str]:
|
def _format_part_values(vinfo: version.V2VersionInfo) -> typ.Dict[str, str]:
|
||||||
"""Generate kwargs for template from minimal VersionInfo.
|
"""Generate kwargs for template from minimal V2VersionInfo.
|
||||||
|
|
||||||
The VersionInfo Tuple only has the minimal representation
|
The V2VersionInfo Tuple only has the minimal representation
|
||||||
of a parsed version, not the values suitable for formatting.
|
of a parsed version, not the values suitable for formatting.
|
||||||
It may for example have month=9, but not the formatted
|
It may for example have month=9, but not the formatted
|
||||||
representation '09' for '0M'.
|
representation '09' for '0M'.
|
||||||
|
|
@ -402,18 +302,20 @@ def _format_part_values(vinfo: VersionInfo) -> typ.Dict[str, str]:
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
|
|
||||||
def _make_segments(pattern: str) -> typ.List[str]:
|
def _make_segments(raw_pattern: str) -> typ.List[str]:
|
||||||
pattern_segs_l: typ.List[str] = []
|
pattern_segs_l: typ.List[str] = []
|
||||||
pattern_segs_r: typ.List[str] = []
|
pattern_segs_r: typ.List[str] = []
|
||||||
|
|
||||||
pattern_rest = pattern
|
pattern_rest = raw_pattern
|
||||||
while "[" in pattern_rest and "]" in pattern_rest:
|
while "[" in pattern_rest and "]" in pattern_rest:
|
||||||
try:
|
try:
|
||||||
seg_l , pattern_rest = pattern_rest.split("[", 1)
|
seg_l , pattern_rest = pattern_rest.split("[", 1)
|
||||||
pattern_rest, seg_r = pattern_rest.rsplit("]", 1)
|
pattern_rest, seg_r = pattern_rest.rsplit("]", 1)
|
||||||
except ValueError as val_err:
|
except ValueError as val_err:
|
||||||
if "values to unpack" in str(val_err):
|
if "values to unpack" in str(val_err):
|
||||||
pat_err = PatternError(f"Unbalanced braces [] in '{pattern}'")
|
err = f"Unbalanced braces [] in '{raw_pattern}'"
|
||||||
|
pat_err = version.PatternError(err)
|
||||||
|
|
||||||
pat_err.__cause__ = val_err
|
pat_err.__cause__ = val_err
|
||||||
raise pat_err
|
raise pat_err
|
||||||
else:
|
else:
|
||||||
|
|
@ -444,7 +346,7 @@ def _clear_zero_segments(
|
||||||
|
|
||||||
|
|
||||||
def _format_segments(
|
def _format_segments(
|
||||||
vinfo : VersionInfo,
|
vinfo : version.V2VersionInfo,
|
||||||
pattern_segs: typ.List[str],
|
pattern_segs: typ.List[str],
|
||||||
) -> typ.List[str]:
|
) -> typ.List[str]:
|
||||||
kwargs = _format_part_values(vinfo)
|
kwargs = _format_part_values(vinfo)
|
||||||
|
|
@ -470,12 +372,12 @@ def _format_segments(
|
||||||
for part, part_value in part_values:
|
for part, part_value in part_values:
|
||||||
if part in seg_l:
|
if part in seg_l:
|
||||||
seg_l = seg_l.replace(part, part_value)
|
seg_l = seg_l.replace(part, part_value)
|
||||||
if not (is_optional and str(part_value) == ZERO_VALUES.get(part)):
|
if not (is_optional and str(part_value) == version.ZERO_VALUES.get(part)):
|
||||||
is_zero_segment[idx_l] = False
|
is_zero_segment[idx_l] = False
|
||||||
|
|
||||||
if part in seg_r:
|
if part in seg_r:
|
||||||
seg_r = seg_r.replace(part, part_value)
|
seg_r = seg_r.replace(part, part_value)
|
||||||
if not (is_optional and str(part_value) == ZERO_VALUES[part]):
|
if not (is_optional and str(part_value) == version.ZERO_VALUES[part]):
|
||||||
is_zero_segment[idx_r] = False
|
is_zero_segment[idx_r] = False
|
||||||
|
|
||||||
formatted_segs_l.append(seg_l)
|
formatted_segs_l.append(seg_l)
|
||||||
|
|
@ -489,7 +391,7 @@ def _format_segments(
|
||||||
return _clear_zero_segments(formatted_segs, is_zero_segment)
|
return _clear_zero_segments(formatted_segs, is_zero_segment)
|
||||||
|
|
||||||
|
|
||||||
def format_version(vinfo: VersionInfo, pattern: str) -> str:
|
def format_version(vinfo: version.V2VersionInfo, raw_pattern: str) -> str:
|
||||||
"""Generate version string.
|
"""Generate version string.
|
||||||
|
|
||||||
>>> import datetime as dt
|
>>> import datetime as dt
|
||||||
|
|
@ -575,7 +477,7 @@ def format_version(vinfo: VersionInfo, pattern: str) -> str:
|
||||||
>>> format_version(vinfo_d, pattern='__version__ = "vMAJOR[.MINOR[.PATCH[-TAG[NUM]]]]"')
|
>>> format_version(vinfo_d, pattern='__version__ = "vMAJOR[.MINOR[.PATCH[-TAG[NUM]]]]"')
|
||||||
'__version__ = "v1.0.0-rc2"'
|
'__version__ = "v1.0.0-rc2"'
|
||||||
"""
|
"""
|
||||||
pattern_segs = _make_segments(pattern)
|
pattern_segs = _make_segments(raw_pattern)
|
||||||
formatted_segs = _format_segments(vinfo, pattern_segs)
|
formatted_segs = _format_segments(vinfo, pattern_segs)
|
||||||
|
|
||||||
return "".join(formatted_segs)
|
return "".join(formatted_segs)
|
||||||
|
|
@ -583,9 +485,9 @@ def format_version(vinfo: VersionInfo, pattern: str) -> str:
|
||||||
|
|
||||||
def incr(
|
def incr(
|
||||||
old_version: str,
|
old_version: str,
|
||||||
pattern : str = "vYYYY0M.BUILD[-TAG]",
|
raw_pattern: str = "vYYYY0M.BUILD[-TAG]",
|
||||||
*,
|
*,
|
||||||
release : str = None,
|
release : typ.Optional[str] = None,
|
||||||
major : bool = False,
|
major : bool = False,
|
||||||
minor : bool = False,
|
minor : bool = False,
|
||||||
patch : bool = False,
|
patch : bool = False,
|
||||||
|
|
@ -593,44 +495,30 @@ def incr(
|
||||||
) -> typ.Optional[str]:
|
) -> typ.Optional[str]:
|
||||||
"""Increment version string.
|
"""Increment version string.
|
||||||
|
|
||||||
'old_version' is assumed to be a string that matches 'pattern'
|
'old_version' is assumed to be a string that matches 'raw_pattern'
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
old_vinfo = parse_version_info(old_version, pattern)
|
old_vinfo = parse_version_info(old_version, raw_pattern)
|
||||||
except PatternError as ex:
|
except version.PatternError as ex:
|
||||||
logger.error(str(ex))
|
logger.error(str(ex))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
cur_vinfo = old_vinfo
|
cur_cinfo = _ver_to_cal_info(old_vinfo) if pin_date else cal_info()
|
||||||
|
|
||||||
cur_cal_nfo = _ver_to_cal_info(old_vinfo) if pin_date else cal_info()
|
if _is_later_than(old_vinfo, cur_cinfo):
|
||||||
|
|
||||||
old_date = (old_vinfo.year_y or 0 , old_vinfo.month or 0 , old_vinfo.dom or 0)
|
|
||||||
cur_date = (cur_cal_nfo.year_y or 0, cur_cal_nfo.month or 0, cur_cal_nfo.dom or 0)
|
|
||||||
|
|
||||||
if old_date <= cur_date:
|
|
||||||
cur_vinfo = cur_vinfo._replace(**cur_cal_nfo._asdict())
|
|
||||||
else:
|
|
||||||
logger.warning(f"Version appears to be from the future '{old_version}'")
|
logger.warning(f"Version appears to be from the future '{old_version}'")
|
||||||
|
cur_vinfo = old_vinfo
|
||||||
|
else:
|
||||||
|
cur_vinfo = old_vinfo._replace(**cur_cinfo._asdict())
|
||||||
|
|
||||||
_bid = cur_vinfo.bid
|
cur_vinfo = version.incr_non_cal_parts(
|
||||||
if int(_bid) < 1000:
|
cur_vinfo,
|
||||||
# prevent truncation of leading zeros
|
release,
|
||||||
_bid = str(int(_bid) + 1000)
|
major,
|
||||||
|
minor,
|
||||||
cur_vinfo = cur_vinfo._replace(bid=lexid.incr(_bid))
|
patch,
|
||||||
|
)
|
||||||
if major:
|
new_version = format_version(cur_vinfo, raw_pattern)
|
||||||
cur_vinfo = cur_vinfo._replace(major=cur_vinfo.major + 1, minor=0, patch=0)
|
|
||||||
if minor:
|
|
||||||
cur_vinfo = cur_vinfo._replace(minor=cur_vinfo.minor + 1, patch=0)
|
|
||||||
if patch:
|
|
||||||
cur_vinfo = cur_vinfo._replace(patch=cur_vinfo.patch + 1)
|
|
||||||
|
|
||||||
if release:
|
|
||||||
cur_vinfo = cur_vinfo._replace(tag=release)
|
|
||||||
|
|
||||||
new_version = format_version(cur_vinfo, pattern)
|
|
||||||
if new_version == old_version:
|
if new_version == old_version:
|
||||||
logger.error("Invalid arguments or pattern, version did not change.")
|
logger.error("Invalid arguments or pattern, version did not change.")
|
||||||
return None
|
return None
|
||||||
|
|
@ -1,30 +1,13 @@
|
||||||
# This file is part of the pycalver project
|
|
||||||
# https://github.com/mbarkhau/pycalver
|
|
||||||
#
|
|
||||||
# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
|
|
||||||
# SPDX-License-Identifier: MIT
|
|
||||||
"""Functions related to version string manipulation."""
|
|
||||||
|
|
||||||
import typing as typ
|
import typing as typ
|
||||||
import logging
|
|
||||||
import datetime as dt
|
import datetime as dt
|
||||||
|
|
||||||
import lexid
|
import lexid
|
||||||
import pkg_resources
|
import pkg_resources
|
||||||
|
|
||||||
import pycalver.patterns as v1patterns
|
|
||||||
|
|
||||||
logger = logging.getLogger("pycalver.version")
|
|
||||||
|
|
||||||
|
|
||||||
# The test suite may replace this.
|
|
||||||
TODAY = dt.datetime.utcnow().date()
|
|
||||||
|
|
||||||
|
|
||||||
MaybeInt = typ.Optional[int]
|
MaybeInt = typ.Optional[int]
|
||||||
|
|
||||||
|
|
||||||
class CalendarInfo(typ.NamedTuple):
|
class V1CalendarInfo(typ.NamedTuple):
|
||||||
"""Container for calendar components of version strings."""
|
"""Container for calendar components of version strings."""
|
||||||
|
|
||||||
year : MaybeInt
|
year : MaybeInt
|
||||||
|
|
@ -36,7 +19,7 @@ class CalendarInfo(typ.NamedTuple):
|
||||||
us_week : MaybeInt
|
us_week : MaybeInt
|
||||||
|
|
||||||
|
|
||||||
class VersionInfo(typ.NamedTuple):
|
class V1VersionInfo(typ.NamedTuple):
|
||||||
"""Container for parsed version string."""
|
"""Container for parsed version string."""
|
||||||
|
|
||||||
year : MaybeInt
|
year : MaybeInt
|
||||||
|
|
@ -53,26 +36,94 @@ class VersionInfo(typ.NamedTuple):
|
||||||
tag : str
|
tag : str
|
||||||
|
|
||||||
|
|
||||||
def _ver_to_cal_info(vinfo: VersionInfo) -> CalendarInfo:
|
class V2CalendarInfo(typ.NamedTuple):
|
||||||
return CalendarInfo(
|
"""Container for calendar components of version strings."""
|
||||||
vinfo.year,
|
|
||||||
vinfo.quarter,
|
year_y : MaybeInt
|
||||||
vinfo.month,
|
year_g : MaybeInt
|
||||||
vinfo.dom,
|
quarter: MaybeInt
|
||||||
vinfo.doy,
|
month : MaybeInt
|
||||||
vinfo.iso_week,
|
dom : MaybeInt
|
||||||
vinfo.us_week,
|
doy : MaybeInt
|
||||||
)
|
week_w : MaybeInt
|
||||||
|
week_u : MaybeInt
|
||||||
|
week_v : MaybeInt
|
||||||
|
|
||||||
|
|
||||||
def _date_from_doy(year: int, doy: int) -> dt.date:
|
class V2VersionInfo(typ.NamedTuple):
|
||||||
|
"""Container for parsed version string."""
|
||||||
|
|
||||||
|
year_y : MaybeInt
|
||||||
|
year_g : MaybeInt
|
||||||
|
quarter: MaybeInt
|
||||||
|
month : MaybeInt
|
||||||
|
dom : MaybeInt
|
||||||
|
doy : MaybeInt
|
||||||
|
week_w : MaybeInt
|
||||||
|
week_u : MaybeInt
|
||||||
|
week_v : MaybeInt
|
||||||
|
major : int
|
||||||
|
minor : int
|
||||||
|
patch : int
|
||||||
|
num : int
|
||||||
|
bid : str
|
||||||
|
tag : str
|
||||||
|
pytag : str
|
||||||
|
|
||||||
|
|
||||||
|
VersionInfoType = typ.TypeVar('VersionInfoType', V1VersionInfo, V2VersionInfo)
|
||||||
|
|
||||||
|
|
||||||
|
# The test suite may replace this.
|
||||||
|
TODAY = dt.datetime.utcnow().date()
|
||||||
|
|
||||||
|
|
||||||
|
TAG_BY_PEP440_TAG = {
|
||||||
|
'a' : 'alpha',
|
||||||
|
'b' : 'beta',
|
||||||
|
"" : 'final',
|
||||||
|
'rc' : 'rc',
|
||||||
|
'dev' : 'dev',
|
||||||
|
'post': 'post',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
PEP440_TAG_BY_TAG = {
|
||||||
|
'alpha': "a",
|
||||||
|
'beta' : "b",
|
||||||
|
'final': "",
|
||||||
|
'pre' : "rc",
|
||||||
|
'rc' : "rc",
|
||||||
|
'dev' : "dev",
|
||||||
|
'post' : "post",
|
||||||
|
}
|
||||||
|
|
||||||
|
assert set(TAG_BY_PEP440_TAG.keys()) == set(PEP440_TAG_BY_TAG.values())
|
||||||
|
assert set(TAG_BY_PEP440_TAG.values()) < set(PEP440_TAG_BY_TAG.keys())
|
||||||
|
|
||||||
|
|
||||||
|
ZERO_VALUES = {
|
||||||
|
'MAJOR': "0",
|
||||||
|
'MINOR': "0",
|
||||||
|
'PATCH': "0",
|
||||||
|
'TAG' : "final",
|
||||||
|
'PYTAG': "",
|
||||||
|
'NUM' : "0",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class PatternError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def date_from_doy(year: int, doy: int) -> dt.date:
|
||||||
"""Parse date from year and day of year (1 indexed).
|
"""Parse date from year and day of year (1 indexed).
|
||||||
|
|
||||||
>>> cases = [
|
>>> cases = [
|
||||||
... (2016, 1), (2016, 31), (2016, 31 + 1), (2016, 31 + 29), (2016, 31 + 30),
|
... (2016, 1), (2016, 31), (2016, 31 + 1), (2016, 31 + 29), (2016, 31 + 30),
|
||||||
... (2017, 1), (2017, 31), (2017, 31 + 1), (2017, 31 + 28), (2017, 31 + 29),
|
... (2017, 1), (2017, 31), (2017, 31 + 1), (2017, 31 + 28), (2017, 31 + 29),
|
||||||
... ]
|
... ]
|
||||||
>>> dates = [_date_from_doy(year, month) for year, month in cases]
|
>>> dates = [date_from_doy(year, month) for year, month in cases]
|
||||||
>>> assert [(d.month, d.day) for d in dates] == [
|
>>> assert [(d.month, d.day) for d in dates] == [
|
||||||
... (1, 1), (1, 31), (2, 1), (2, 29), (3, 1),
|
... (1, 1), (1, 31), (2, 1), (2, 29), (3, 1),
|
||||||
... (1, 1), (1, 31), (2, 1), (2, 28), (3, 1),
|
... (1, 1), (1, 31), (2, 1), (2, 28), (3, 1),
|
||||||
|
|
@ -81,407 +132,15 @@ def _date_from_doy(year: int, doy: int) -> dt.date:
|
||||||
return dt.date(year, 1, 1) + dt.timedelta(days=doy - 1)
|
return dt.date(year, 1, 1) + dt.timedelta(days=doy - 1)
|
||||||
|
|
||||||
|
|
||||||
def _quarter_from_month(month: int) -> int:
|
def quarter_from_month(month: int) -> int:
|
||||||
"""Calculate quarter (1 indexed) from month (1 indexed).
|
"""Calculate quarter (1 indexed) from month (1 indexed).
|
||||||
|
|
||||||
>>> [_quarter_from_month(month) for month in range(1, 13)]
|
>>> [quarter_from_month(month) for month in range(1, 13)]
|
||||||
[1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4]
|
[1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4]
|
||||||
"""
|
"""
|
||||||
return ((month - 1) // 3) + 1
|
return ((month - 1) // 3) + 1
|
||||||
|
|
||||||
|
|
||||||
def cal_info(date: dt.date = None) -> CalendarInfo:
|
|
||||||
"""Generate calendar components for current date.
|
|
||||||
|
|
||||||
>>> from datetime import date
|
|
||||||
|
|
||||||
>>> c = cal_info(date(2019, 1, 5))
|
|
||||||
>>> (c.year, c.quarter, c.month, c.dom, c.doy, c.iso_week, c.us_week)
|
|
||||||
(2019, 1, 1, 5, 5, 0, 0)
|
|
||||||
|
|
||||||
>>> c = cal_info(date(2019, 1, 6))
|
|
||||||
>>> (c.year, c.quarter, c.month, c.dom, c.doy, c.iso_week, c.us_week)
|
|
||||||
(2019, 1, 1, 6, 6, 0, 1)
|
|
||||||
|
|
||||||
>>> c = cal_info(date(2019, 1, 7))
|
|
||||||
>>> (c.year, c.quarter, c.month, c.dom, c.doy, c.iso_week, c.us_week)
|
|
||||||
(2019, 1, 1, 7, 7, 1, 1)
|
|
||||||
|
|
||||||
>>> c = cal_info(date(2019, 4, 7))
|
|
||||||
>>> (c.year, c.quarter, c.month, c.dom, c.doy, c.iso_week, c.us_week)
|
|
||||||
(2019, 2, 4, 7, 97, 13, 14)
|
|
||||||
"""
|
|
||||||
if date is None:
|
|
||||||
date = TODAY
|
|
||||||
|
|
||||||
kwargs = {
|
|
||||||
'year' : date.year,
|
|
||||||
'quarter' : _quarter_from_month(date.month),
|
|
||||||
'month' : date.month,
|
|
||||||
'dom' : date.day,
|
|
||||||
'doy' : int(date.strftime("%j"), base=10),
|
|
||||||
'iso_week': int(date.strftime("%W"), base=10),
|
|
||||||
'us_week' : int(date.strftime("%U"), base=10),
|
|
||||||
}
|
|
||||||
|
|
||||||
return CalendarInfo(**kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
FieldKey = str
|
|
||||||
MatchGroupKey = str
|
|
||||||
MatchGroupStr = str
|
|
||||||
|
|
||||||
PatternGroups = typ.Dict[MatchGroupKey, MatchGroupStr]
|
|
||||||
FieldValues = typ.Dict[FieldKey , MatchGroupStr]
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_field_values(field_values: FieldValues) -> VersionInfo:
|
|
||||||
fvals = field_values
|
|
||||||
tag = fvals.get('tag')
|
|
||||||
if tag is None:
|
|
||||||
tag = "final"
|
|
||||||
tag = TAG_ALIASES.get(tag, tag)
|
|
||||||
assert tag is not None
|
|
||||||
|
|
||||||
bid = fvals['bid'] if 'bid' in fvals else "0001"
|
|
||||||
|
|
||||||
year = int(fvals['year']) if 'year' in fvals else None
|
|
||||||
doy = int(fvals['doy' ]) if 'doy' in fvals else None
|
|
||||||
|
|
||||||
month: typ.Optional[int]
|
|
||||||
dom : typ.Optional[int]
|
|
||||||
|
|
||||||
if year and doy:
|
|
||||||
date = _date_from_doy(year, doy)
|
|
||||||
month = date.month
|
|
||||||
dom = date.day
|
|
||||||
else:
|
|
||||||
month = int(fvals['month']) if 'month' in fvals else None
|
|
||||||
dom = int(fvals['dom' ]) if 'dom' in fvals else None
|
|
||||||
|
|
||||||
iso_week: typ.Optional[int]
|
|
||||||
us_week : typ.Optional[int]
|
|
||||||
|
|
||||||
if year and month and dom:
|
|
||||||
date = dt.date(year, month, dom)
|
|
||||||
doy = int(date.strftime("%j"), base=10)
|
|
||||||
iso_week = int(date.strftime("%W"), base=10)
|
|
||||||
us_week = int(date.strftime("%U"), base=10)
|
|
||||||
else:
|
|
||||||
iso_week = None
|
|
||||||
us_week = None
|
|
||||||
|
|
||||||
quarter = int(fvals['quarter']) if 'quarter' in fvals else None
|
|
||||||
if quarter is None and month:
|
|
||||||
quarter = _quarter_from_month(month)
|
|
||||||
|
|
||||||
major = int(fvals['major']) if 'major' in fvals else 0
|
|
||||||
minor = int(fvals['minor']) if 'minor' in fvals else 0
|
|
||||||
patch = int(fvals['patch']) if 'patch' in fvals else 0
|
|
||||||
|
|
||||||
return VersionInfo(
|
|
||||||
year=year,
|
|
||||||
quarter=quarter,
|
|
||||||
month=month,
|
|
||||||
dom=dom,
|
|
||||||
doy=doy,
|
|
||||||
iso_week=iso_week,
|
|
||||||
us_week=us_week,
|
|
||||||
major=major,
|
|
||||||
minor=minor,
|
|
||||||
patch=patch,
|
|
||||||
bid=bid,
|
|
||||||
tag=tag,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _is_calver(nfo: typ.Union[CalendarInfo, VersionInfo]) -> bool:
|
|
||||||
"""Check pattern for any calendar based parts.
|
|
||||||
|
|
||||||
>>> _is_calver(cal_info())
|
|
||||||
True
|
|
||||||
|
|
||||||
>>> vnfo = _parse_version_info({'year': "2018", 'month': "11", 'bid': "0018"})
|
|
||||||
>>> _is_calver(vnfo)
|
|
||||||
True
|
|
||||||
|
|
||||||
>>> vnfo = _parse_version_info({'MAJOR': "1", 'MINOR': "023", 'PATCH': "45"})
|
|
||||||
>>> _is_calver(vnfo)
|
|
||||||
False
|
|
||||||
"""
|
|
||||||
for field in CalendarInfo._fields:
|
|
||||||
maybe_val: typ.Any = getattr(nfo, field, None)
|
|
||||||
if isinstance(maybe_val, int):
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
TAG_ALIASES: typ.Dict[str, str] = {'a': "alpha", 'b': "beta", 'pre': "rc"}
|
|
||||||
|
|
||||||
|
|
||||||
PEP440_TAGS: typ.Dict[str, str] = {
|
|
||||||
'alpha': "a",
|
|
||||||
'beta' : "b",
|
|
||||||
'final': "",
|
|
||||||
'rc' : "rc",
|
|
||||||
'dev' : "dev",
|
|
||||||
'post' : "post",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
VersionInfoKW = typ.Dict[str, typ.Union[str, int, None]]
|
|
||||||
|
|
||||||
|
|
||||||
class PatternError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_pattern_groups(pattern_groups: PatternGroups) -> FieldValues:
|
|
||||||
for part_name in pattern_groups.keys():
|
|
||||||
is_valid_part_name = (
|
|
||||||
part_name in v1patterns.COMPOSITE_PART_PATTERNS
|
|
||||||
or part_name in v1patterns.PATTERN_PART_FIELDS
|
|
||||||
)
|
|
||||||
if not is_valid_part_name:
|
|
||||||
err_msg = f"Invalid part '{part_name}'"
|
|
||||||
raise PatternError(err_msg)
|
|
||||||
|
|
||||||
field_value_items = [
|
|
||||||
(field_name, pattern_groups[part_name])
|
|
||||||
for part_name, field_name in v1patterns.PATTERN_PART_FIELDS.items()
|
|
||||||
if part_name in pattern_groups.keys()
|
|
||||||
]
|
|
||||||
|
|
||||||
all_fields = [field_name for field_name, _ in field_value_items]
|
|
||||||
unique_fields = set(all_fields)
|
|
||||||
duplicate_fields = [f for f in unique_fields if all_fields.count(f) > 1]
|
|
||||||
|
|
||||||
if any(duplicate_fields):
|
|
||||||
err_msg = f"Multiple parts for same field {duplicate_fields}."
|
|
||||||
raise PatternError(err_msg)
|
|
||||||
else:
|
|
||||||
return dict(field_value_items)
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_version_info(pattern_groups: PatternGroups) -> VersionInfo:
|
|
||||||
"""Parse normalized VersionInfo from groups of a matched pattern.
|
|
||||||
|
|
||||||
>>> vnfo = _parse_version_info({'year': "2018", 'month': "11", 'bid': "0099"})
|
|
||||||
>>> (vnfo.year, vnfo.month, vnfo.quarter, vnfo.bid, vnfo.tag)
|
|
||||||
(2018, 11, 4, '0099', 'final')
|
|
||||||
|
|
||||||
>>> vnfo = _parse_version_info({'year': "2018", 'doy': "11", 'bid': "099", 'tag': "b"})
|
|
||||||
>>> (vnfo.year, vnfo.month, vnfo.dom, vnfo.bid, vnfo.tag)
|
|
||||||
(2018, 1, 11, '099', 'beta')
|
|
||||||
|
|
||||||
>>> vnfo = _parse_version_info({'MAJOR': "1", 'MINOR': "23", 'PATCH': "45"})
|
|
||||||
>>> (vnfo.major, vnfo.minor, vnfo.patch)
|
|
||||||
(1, 23, 45)
|
|
||||||
|
|
||||||
>>> vnfo = _parse_version_info({'MAJOR': "1", 'MMM': "023", 'PPPP': "0045"})
|
|
||||||
>>> (vnfo.major, vnfo.minor, vnfo.patch)
|
|
||||||
(1, 23, 45)
|
|
||||||
"""
|
|
||||||
field_values = _parse_pattern_groups(pattern_groups)
|
|
||||||
return _parse_field_values(field_values)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_version_info(version_str: str, pattern: str = "{pycalver}") -> VersionInfo:
|
|
||||||
"""Parse normalized VersionInfo.
|
|
||||||
|
|
||||||
>>> vnfo = parse_version_info("v201712.0033-beta", pattern="{pycalver}")
|
|
||||||
>>> assert vnfo == _parse_version_info({'year': 2017, 'month': 12, 'bid': "0033", 'tag': "beta"})
|
|
||||||
|
|
||||||
>>> vnfo = parse_version_info("1.23.456", pattern="{semver}")
|
|
||||||
>>> assert vnfo == _parse_version_info({'MAJOR': "1", 'MINOR': "23", 'PATCH': "456"})
|
|
||||||
"""
|
|
||||||
pattern_tup = v1patterns.compile_pattern(pattern)
|
|
||||||
match = pattern_tup.regexp.match(version_str)
|
|
||||||
if match is None:
|
|
||||||
err_msg = (
|
|
||||||
f"Invalid version string '{version_str}' "
|
|
||||||
f"for pattern '{pattern}'/'{pattern_tup.regexp.pattern}'"
|
|
||||||
)
|
|
||||||
raise PatternError(err_msg)
|
|
||||||
else:
|
|
||||||
return _parse_version_info(match.groupdict())
|
|
||||||
|
|
||||||
|
|
||||||
def is_valid(version_str: str, pattern: str = "{pycalver}") -> bool:
|
|
||||||
"""Check if a version matches a pattern.
|
|
||||||
|
|
||||||
>>> is_valid("v201712.0033-beta", pattern="{pycalver}")
|
|
||||||
True
|
|
||||||
>>> is_valid("v201712.0033-beta", pattern="{semver}")
|
|
||||||
False
|
|
||||||
>>> is_valid("1.2.3", pattern="{semver}")
|
|
||||||
True
|
|
||||||
>>> is_valid("v201712.0033-beta", pattern="{semver}")
|
|
||||||
False
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
parse_version_info(version_str, pattern)
|
|
||||||
return True
|
|
||||||
except PatternError:
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
ID_FIELDS_BY_PART = {
|
|
||||||
'MAJOR' : 'major',
|
|
||||||
'MINOR' : 'minor',
|
|
||||||
'MM' : 'minor',
|
|
||||||
'MMM' : 'minor',
|
|
||||||
'MMMM' : 'minor',
|
|
||||||
'MMMMM' : 'minor',
|
|
||||||
'MMMMMM' : 'minor',
|
|
||||||
'MMMMMMM': 'minor',
|
|
||||||
'PATCH' : 'patch',
|
|
||||||
'PP' : 'patch',
|
|
||||||
'PPP' : 'patch',
|
|
||||||
'PPPP' : 'patch',
|
|
||||||
'PPPPP' : 'patch',
|
|
||||||
'PPPPPP' : 'patch',
|
|
||||||
'PPPPPPP': 'patch',
|
|
||||||
'BID' : 'bid',
|
|
||||||
'BB' : 'bid',
|
|
||||||
'BBB' : 'bid',
|
|
||||||
'BBBB' : 'bid',
|
|
||||||
'BBBBB' : 'bid',
|
|
||||||
'BBBBBB' : 'bid',
|
|
||||||
'BBBBBBB': 'bid',
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def format_version(vinfo: VersionInfo, pattern: str) -> str:
|
|
||||||
"""Generate version string.
|
|
||||||
|
|
||||||
>>> import datetime as dt
|
|
||||||
>>> vinfo = parse_version_info("v201712.0033-beta", pattern="{pycalver}")
|
|
||||||
>>> vinfo_a = vinfo._replace(**cal_info(date=dt.date(2017, 1, 1))._asdict())
|
|
||||||
>>> vinfo_b = vinfo._replace(**cal_info(date=dt.date(2017, 12, 31))._asdict())
|
|
||||||
>>> vinfo_c = vinfo_b._replace(major=1, minor=2, patch=34, tag='final')
|
|
||||||
|
|
||||||
>>> format_version(vinfo_a, pattern="v{yy}.{BID}{release}")
|
|
||||||
'v17.33-beta'
|
|
||||||
>>> format_version(vinfo_a, pattern="{pep440_version}")
|
|
||||||
'201701.33b0'
|
|
||||||
|
|
||||||
>>> format_version(vinfo_a, pattern="{pycalver}")
|
|
||||||
'v201701.0033-beta'
|
|
||||||
>>> format_version(vinfo_b, pattern="{pycalver}")
|
|
||||||
'v201712.0033-beta'
|
|
||||||
|
|
||||||
>>> format_version(vinfo_a, pattern="v{year}w{iso_week}.{BID}{release}")
|
|
||||||
'v2017w00.33-beta'
|
|
||||||
>>> format_version(vinfo_b, pattern="v{year}w{iso_week}.{BID}{release}")
|
|
||||||
'v2017w52.33-beta'
|
|
||||||
|
|
||||||
>>> format_version(vinfo_a, pattern="v{year}d{doy}.{bid}{release}")
|
|
||||||
'v2017d001.0033-beta'
|
|
||||||
>>> format_version(vinfo_b, pattern="v{year}d{doy}.{bid}{release}")
|
|
||||||
'v2017d365.0033-beta'
|
|
||||||
|
|
||||||
>>> format_version(vinfo_c, pattern="v{year}w{iso_week}.{BID}-{tag}")
|
|
||||||
'v2017w52.33-final'
|
|
||||||
>>> format_version(vinfo_c, pattern="v{year}w{iso_week}.{BID}{release}")
|
|
||||||
'v2017w52.33'
|
|
||||||
|
|
||||||
>>> format_version(vinfo_c, pattern="v{MAJOR}.{MINOR}.{PATCH}")
|
|
||||||
'v1.2.34'
|
|
||||||
>>> format_version(vinfo_c, pattern="v{MAJOR}.{MM}.{PPP}")
|
|
||||||
'v1.02.034'
|
|
||||||
"""
|
|
||||||
full_pattern = pattern
|
|
||||||
for part_name, full_part_format in v1patterns.FULL_PART_FORMATS.items():
|
|
||||||
full_pattern = full_pattern.replace("{" + part_name + "}", full_part_format)
|
|
||||||
|
|
||||||
kwargs: typ.Dict[str, typ.Union[str, int, None]] = vinfo._asdict()
|
|
||||||
|
|
||||||
tag = vinfo.tag
|
|
||||||
if tag == 'final':
|
|
||||||
kwargs['release' ] = ""
|
|
||||||
kwargs['pep440_tag'] = ""
|
|
||||||
else:
|
|
||||||
kwargs['release' ] = "-" + tag
|
|
||||||
kwargs['pep440_tag'] = PEP440_TAGS[tag] + "0"
|
|
||||||
|
|
||||||
kwargs['release_tag'] = tag
|
|
||||||
|
|
||||||
year = vinfo.year
|
|
||||||
if year:
|
|
||||||
kwargs['yy' ] = str(year)[-2:]
|
|
||||||
kwargs['yyyy'] = year
|
|
||||||
|
|
||||||
kwargs['BID'] = int(vinfo.bid, 10)
|
|
||||||
|
|
||||||
for part_name, field in ID_FIELDS_BY_PART.items():
|
|
||||||
val = kwargs[field]
|
|
||||||
if part_name.lower() == field.lower():
|
|
||||||
if isinstance(val, str):
|
|
||||||
kwargs[part_name] = int(val, base=10)
|
|
||||||
else:
|
|
||||||
kwargs[part_name] = val
|
|
||||||
else:
|
|
||||||
assert len(set(part_name)) == 1
|
|
||||||
padded_len = len(part_name)
|
|
||||||
kwargs[part_name] = str(val).zfill(padded_len)
|
|
||||||
|
|
||||||
return full_pattern.format(**kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
def incr(
|
|
||||||
old_version: str,
|
|
||||||
pattern : str = "{pycalver}",
|
|
||||||
*,
|
|
||||||
release : str = None,
|
|
||||||
major : bool = False,
|
|
||||||
minor : bool = False,
|
|
||||||
patch : bool = False,
|
|
||||||
pin_date: bool = False,
|
|
||||||
) -> typ.Optional[str]:
|
|
||||||
"""Increment version string.
|
|
||||||
|
|
||||||
'old_version' is assumed to be a string that matches 'pattern'
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
old_vinfo = parse_version_info(old_version, pattern)
|
|
||||||
except PatternError as ex:
|
|
||||||
logger.error(str(ex))
|
|
||||||
return None
|
|
||||||
|
|
||||||
cur_vinfo = old_vinfo
|
|
||||||
|
|
||||||
cur_cal_nfo = _ver_to_cal_info(old_vinfo) if pin_date else cal_info()
|
|
||||||
|
|
||||||
old_date = (old_vinfo.year or 0 , old_vinfo.month or 0 , old_vinfo.dom or 0)
|
|
||||||
cur_date = (cur_cal_nfo.year or 0, cur_cal_nfo.month or 0, cur_cal_nfo.dom or 0)
|
|
||||||
|
|
||||||
if old_date <= cur_date:
|
|
||||||
cur_vinfo = cur_vinfo._replace(**cur_cal_nfo._asdict())
|
|
||||||
else:
|
|
||||||
logger.warning(f"Version appears to be from the future '{old_version}'")
|
|
||||||
|
|
||||||
cur_vinfo = cur_vinfo._replace(bid=lexid.next_id(cur_vinfo.bid))
|
|
||||||
|
|
||||||
if major:
|
|
||||||
cur_vinfo = cur_vinfo._replace(major=cur_vinfo.major + 1, minor=0, patch=0)
|
|
||||||
if minor:
|
|
||||||
cur_vinfo = cur_vinfo._replace(minor=cur_vinfo.minor + 1, patch=0)
|
|
||||||
if patch:
|
|
||||||
cur_vinfo = cur_vinfo._replace(patch=cur_vinfo.patch + 1)
|
|
||||||
|
|
||||||
if release:
|
|
||||||
cur_vinfo = cur_vinfo._replace(tag=release)
|
|
||||||
|
|
||||||
new_version = format_version(cur_vinfo, pattern)
|
|
||||||
if new_version == old_version:
|
|
||||||
logger.error("Invalid arguments or pattern, version did not change.")
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
return new_version
|
|
||||||
|
|
||||||
|
|
||||||
def to_pep440(version: str) -> str:
|
def to_pep440(version: str) -> str:
|
||||||
"""Derive pep440 compliant version string from PyCalVer version string.
|
"""Derive pep440 compliant version string from PyCalVer version string.
|
||||||
|
|
||||||
|
|
@ -489,3 +148,28 @@ def to_pep440(version: str) -> str:
|
||||||
'201811.7b0'
|
'201811.7b0'
|
||||||
"""
|
"""
|
||||||
return str(pkg_resources.parse_version(version))
|
return str(pkg_resources.parse_version(version))
|
||||||
|
|
||||||
|
|
||||||
|
def incr_non_cal_parts(
|
||||||
|
vinfo : VersionInfoType,
|
||||||
|
release: typ.Optional[str],
|
||||||
|
major : bool,
|
||||||
|
minor : bool,
|
||||||
|
patch : bool,
|
||||||
|
) -> VersionInfoType:
|
||||||
|
_bid = vinfo.bid
|
||||||
|
if int(_bid) < 1000:
|
||||||
|
# prevent truncation of leading zeros
|
||||||
|
_bid = str(int(_bid) + 1000)
|
||||||
|
|
||||||
|
vinfo = vinfo._replace(bid=lexid.next_id(_bid))
|
||||||
|
|
||||||
|
if release:
|
||||||
|
vinfo = vinfo._replace(tag=release)
|
||||||
|
if major:
|
||||||
|
vinfo = vinfo._replace(major=vinfo.major + 1, minor=0, patch=0)
|
||||||
|
if minor:
|
||||||
|
vinfo = vinfo._replace(minor=vinfo.minor + 1, patch=0)
|
||||||
|
if patch:
|
||||||
|
vinfo = vinfo._replace(patch=vinfo.patch + 1)
|
||||||
|
return vinfo
|
||||||
|
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
# This file is part of the pycalver project
|
|
||||||
# https://github.com/mbarkhau/pycalver
|
|
||||||
#
|
|
||||||
# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
|
|
||||||
# SPDX-License-Identifier: MIT
|
|
||||||
"""PyCalVer: CalVer for Python Packages."""
|
|
||||||
|
|
||||||
__version__ = "v202007.1036"
|
|
||||||
|
|
@ -1,178 +0,0 @@
|
||||||
# This file is part of the pycalver project
|
|
||||||
# https://github.com/mbarkhau/pycalver
|
|
||||||
#
|
|
||||||
# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
|
|
||||||
# SPDX-License-Identifier: MIT
|
|
||||||
"""Rewrite files, updating occurences of version strings."""
|
|
||||||
|
|
||||||
import io
|
|
||||||
import typing as typ
|
|
||||||
import logging
|
|
||||||
|
|
||||||
import pycalver.rewrite as v1rewrite
|
|
||||||
import pycalver2.version as v2version
|
|
||||||
import pycalver2.patterns as v2patterns
|
|
||||||
from pycalver import parse
|
|
||||||
from pycalver import config
|
|
||||||
|
|
||||||
logger = logging.getLogger("pycalver2.rewrite")
|
|
||||||
|
|
||||||
|
|
||||||
def rewrite_lines(
|
|
||||||
pattern_strs: typ.List[str],
|
|
||||||
new_vinfo : v2version.VersionInfo,
|
|
||||||
old_lines : typ.List[str],
|
|
||||||
) -> typ.List[str]:
|
|
||||||
"""Replace occurances of pattern_strs in old_lines with new_vinfo.
|
|
||||||
|
|
||||||
>>> new_vinfo = v2version.parse_version_info("v201811.0123-beta")
|
|
||||||
>>> pattern_strs = ['__version__ = "vYYYY0M.BUILD[-TAG]"']
|
|
||||||
>>> old_lines = ['__version__ = "v201809.0002-alpha" ']
|
|
||||||
>>> rewrite_lines(pattern_strs, new_vinfo, old_lines)
|
|
||||||
['__version__ = "v201811.0123-beta" ']
|
|
||||||
|
|
||||||
>>> old_lines = ['__version__ = "v201809.0002-alpha" # comment']
|
|
||||||
>>> rewrite_lines(pattern_strs, new_vinfo, old_lines)
|
|
||||||
['__version__ = "v201811.0123-beta" # comment']
|
|
||||||
|
|
||||||
>>> pattern_strs = ['__version__ = "YYYY0M.BLD[PYTAGNUM]"']
|
|
||||||
>>> old_lines = ['__version__ = "201809.2a0"']
|
|
||||||
>>> rewrite_lines(pattern_strs, new_vinfo, old_lines)
|
|
||||||
['__version__ = "201811.123b0"']
|
|
||||||
"""
|
|
||||||
new_lines = old_lines[:]
|
|
||||||
found_patterns = set()
|
|
||||||
|
|
||||||
patterns = [v2patterns.compile_pattern(p) for p in pattern_strs]
|
|
||||||
matches = parse.iter_matches(old_lines, patterns)
|
|
||||||
for match in matches:
|
|
||||||
found_patterns.add(match.pattern.raw)
|
|
||||||
replacement = v2version.format_version(new_vinfo, match.pattern.raw)
|
|
||||||
span_l, span_r = match.span
|
|
||||||
new_line = match.line[:span_l] + replacement + match.line[span_r:]
|
|
||||||
new_lines[match.lineno] = new_line
|
|
||||||
|
|
||||||
non_matched_patterns = set(pattern_strs) - found_patterns
|
|
||||||
if non_matched_patterns:
|
|
||||||
for non_matched_pattern in non_matched_patterns:
|
|
||||||
logger.error(f"No match for pattern '{non_matched_pattern}'")
|
|
||||||
compiled_pattern_str = v2patterns.compile_pattern_str(non_matched_pattern)
|
|
||||||
logger.error(f"Pattern compiles to regex '{compiled_pattern_str}'")
|
|
||||||
raise v1rewrite.NoPatternMatch("Invalid pattern(s)")
|
|
||||||
else:
|
|
||||||
return new_lines
|
|
||||||
|
|
||||||
|
|
||||||
def rfd_from_content(
|
|
||||||
pattern_strs: typ.List[str],
|
|
||||||
new_vinfo : v2version.VersionInfo,
|
|
||||||
content : str,
|
|
||||||
) -> v1rewrite.RewrittenFileData:
|
|
||||||
r"""Rewrite pattern occurrences with version string.
|
|
||||||
|
|
||||||
>>> new_vinfo = v2version.parse_version_info("v201809.0123")
|
|
||||||
>>> pattern_strs = ['__version__ = "vYYYY0M.BUILD[-TAG]"']
|
|
||||||
>>> content = '__version__ = "v201809.0001-alpha"'
|
|
||||||
>>> rfd = rfd_from_content(pattern_strs, new_vinfo, content)
|
|
||||||
>>> rfd.new_lines
|
|
||||||
['__version__ = "v201809.0123"']
|
|
||||||
>>>
|
|
||||||
>>> new_vinfo = v2version.parse_version_info("v1.2.3", "vMAJOR.MINOR.PATCH")
|
|
||||||
>>> pattern_strs = ['__version__ = "vMAJOR.MINOR.PATCH"']
|
|
||||||
>>> content = '__version__ = "v1.2.2"'
|
|
||||||
>>> rfd = rfd_from_content(pattern_strs, new_vinfo, content)
|
|
||||||
>>> rfd.new_lines
|
|
||||||
['__version__ = "v1.2.3"']
|
|
||||||
"""
|
|
||||||
line_sep = v1rewrite.detect_line_sep(content)
|
|
||||||
old_lines = content.split(line_sep)
|
|
||||||
new_lines = rewrite_lines(pattern_strs, new_vinfo, old_lines)
|
|
||||||
return v1rewrite.RewrittenFileData("<path>", line_sep, old_lines, new_lines)
|
|
||||||
|
|
||||||
|
|
||||||
def iter_rewritten(
|
|
||||||
file_patterns: config.PatternsByGlob,
|
|
||||||
new_vinfo : v2version.VersionInfo,
|
|
||||||
) -> typ.Iterable[v1rewrite.RewrittenFileData]:
|
|
||||||
r'''Iterate over files with version string replaced.
|
|
||||||
|
|
||||||
>>> file_patterns = {"src/pycalver/__init__.py": ['__version__ = "vYYYY0M.BUILD[-TAG]"']}
|
|
||||||
>>> new_vinfo = v2version.parse_version_info("v201809.0123")
|
|
||||||
>>> rewritten_datas = iter_rewritten(file_patterns, new_vinfo)
|
|
||||||
>>> rfd = list(rewritten_datas)[0]
|
|
||||||
>>> assert rfd.new_lines == [
|
|
||||||
... '# This file is part of the pycalver project',
|
|
||||||
... '# https://github.com/mbarkhau/pycalver',
|
|
||||||
... '#',
|
|
||||||
... '# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License',
|
|
||||||
... '# SPDX-License-Identifier: MIT',
|
|
||||||
... '"""PyCalVer: CalVer for Python Packages."""',
|
|
||||||
... '',
|
|
||||||
... '__version__ = "v201809.0123"',
|
|
||||||
... '',
|
|
||||||
... ]
|
|
||||||
>>>
|
|
||||||
'''
|
|
||||||
|
|
||||||
fobj: typ.IO[str]
|
|
||||||
|
|
||||||
for file_path, pattern_strs in v1rewrite.iter_file_paths(file_patterns):
|
|
||||||
with file_path.open(mode="rt", encoding="utf-8") as fobj:
|
|
||||||
content = fobj.read()
|
|
||||||
|
|
||||||
rfd = rfd_from_content(pattern_strs, new_vinfo, content)
|
|
||||||
yield rfd._replace(path=str(file_path))
|
|
||||||
|
|
||||||
|
|
||||||
def diff(
|
|
||||||
new_vinfo : v2version.VersionInfo,
|
|
||||||
file_patterns: config.PatternsByGlob,
|
|
||||||
) -> str:
|
|
||||||
r"""Generate diffs of rewritten files.
|
|
||||||
|
|
||||||
>>> new_vinfo = v2version.parse_version_info("v201809.0123")
|
|
||||||
>>> file_patterns = {"src/pycalver/__init__.py": ['__version__ = "vYYYY0M.BUILD[-TAG]"']}
|
|
||||||
>>> diff_str = diff(new_vinfo, file_patterns)
|
|
||||||
>>> lines = diff_str.split("\n")
|
|
||||||
>>> lines[:2]
|
|
||||||
['--- src/pycalver/__init__.py', '+++ src/pycalver/__init__.py']
|
|
||||||
>>> assert lines[6].startswith('-__version__ = "v2')
|
|
||||||
>>> assert not lines[6].startswith('-__version__ = "v201809.0123"')
|
|
||||||
>>> lines[7]
|
|
||||||
'+__version__ = "v201809.0123"'
|
|
||||||
"""
|
|
||||||
|
|
||||||
full_diff = ""
|
|
||||||
fobj: typ.IO[str]
|
|
||||||
|
|
||||||
for file_path, pattern_strs in sorted(v1rewrite.iter_file_paths(file_patterns)):
|
|
||||||
with file_path.open(mode="rt", encoding="utf-8") as fobj:
|
|
||||||
content = fobj.read()
|
|
||||||
|
|
||||||
try:
|
|
||||||
rfd = rfd_from_content(pattern_strs, new_vinfo, content)
|
|
||||||
except v1rewrite.NoPatternMatch:
|
|
||||||
# pylint:disable=raise-missing-from ; we support py2, so not an option
|
|
||||||
errmsg = f"No patterns matched for '{file_path}'"
|
|
||||||
raise v1rewrite.NoPatternMatch(errmsg)
|
|
||||||
|
|
||||||
rfd = rfd._replace(path=str(file_path))
|
|
||||||
lines = v1rewrite.diff_lines(rfd)
|
|
||||||
if len(lines) == 0:
|
|
||||||
errmsg = f"No patterns matched for '{file_path}'"
|
|
||||||
raise v1rewrite.NoPatternMatch(errmsg)
|
|
||||||
|
|
||||||
full_diff += "\n".join(lines) + "\n"
|
|
||||||
|
|
||||||
full_diff = full_diff.rstrip("\n")
|
|
||||||
return full_diff
|
|
||||||
|
|
||||||
|
|
||||||
def rewrite(file_patterns: config.PatternsByGlob, new_vinfo: v2version.VersionInfo) -> None:
|
|
||||||
"""Rewrite project files, updating each with the new version."""
|
|
||||||
fobj: typ.IO[str]
|
|
||||||
|
|
||||||
for file_data in iter_rewritten(file_patterns, new_vinfo):
|
|
||||||
new_content = file_data.line_sep.join(file_data.new_lines)
|
|
||||||
with io.open(file_data.path, mode="wt", encoding="utf-8") as fobj:
|
|
||||||
fobj.write(new_content)
|
|
||||||
|
|
@ -13,8 +13,8 @@ import pytest
|
||||||
import pathlib2 as pl
|
import pathlib2 as pl
|
||||||
from click.testing import CliRunner
|
from click.testing import CliRunner
|
||||||
|
|
||||||
import pycalver.config as config
|
from pycalver import config
|
||||||
import pycalver.patterns as v1patterns
|
from pycalver import v1patterns
|
||||||
from pycalver.__main__ import cli
|
from pycalver.__main__ import cli
|
||||||
|
|
||||||
# pylint:disable=redefined-outer-name ; pytest fixtures
|
# pylint:disable=redefined-outer-name ; pytest fixtures
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@ from __future__ import print_function
|
||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import pycalver.patterns as v1patterns
|
|
||||||
from pycalver import parse
|
from pycalver import parse
|
||||||
|
from pycalver import v1patterns
|
||||||
|
|
||||||
SETUP_PY_FIXTURE = """
|
SETUP_PY_FIXTURE = """
|
||||||
# setup.py
|
# setup.py
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,8 @@ import re
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
import pycalver.patterns as v1patterns
|
from pycalver import v1patterns
|
||||||
import pycalver2.patterns as v2patterns
|
from pycalver import v2patterns
|
||||||
|
|
||||||
# TODO (mb 2020-09-06): test for v2patterns
|
# TODO (mb 2020-09-06): test for v2patterns
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,11 @@ import copy
|
||||||
from test import util
|
from test import util
|
||||||
|
|
||||||
from pycalver import config
|
from pycalver import config
|
||||||
from pycalver import rewrite as v1rewrite
|
from pycalver import rewrite
|
||||||
from pycalver import version as v1version
|
from pycalver import v1rewrite
|
||||||
from pycalver2 import rewrite as v2rewrite
|
from pycalver import v1version
|
||||||
from pycalver2 import version as v2version
|
from pycalver import v2rewrite
|
||||||
|
from pycalver import v2version
|
||||||
|
|
||||||
# pylint:disable=protected-access ; allowed for test code
|
# pylint:disable=protected-access ; allowed for test code
|
||||||
|
|
||||||
|
|
@ -54,7 +55,7 @@ def test_iter_file_paths():
|
||||||
cfg = config.parse(ctx)
|
cfg = config.parse(ctx)
|
||||||
assert cfg
|
assert cfg
|
||||||
|
|
||||||
_paths_and_patterns = v1rewrite.iter_file_paths(cfg.file_patterns)
|
_paths_and_patterns = rewrite.iter_path_patterns_items(cfg.file_patterns)
|
||||||
file_paths = {str(file_path) for file_path, patterns in _paths_and_patterns}
|
file_paths = {str(file_path) for file_path, patterns in _paths_and_patterns}
|
||||||
|
|
||||||
assert file_paths == {"pycalver.toml", "README.md"}
|
assert file_paths == {"pycalver.toml", "README.md"}
|
||||||
|
|
@ -66,7 +67,7 @@ def test_iter_file_globs():
|
||||||
cfg = config.parse(ctx)
|
cfg = config.parse(ctx)
|
||||||
assert cfg
|
assert cfg
|
||||||
|
|
||||||
_paths_and_patterns = v1rewrite.iter_file_paths(cfg.file_patterns)
|
_paths_and_patterns = rewrite.iter_path_patterns_items(cfg.file_patterns)
|
||||||
file_paths = {str(file_path) for file_path, patterns in _paths_and_patterns}
|
file_paths = {str(file_path) for file_path, patterns in _paths_and_patterns}
|
||||||
|
|
||||||
assert file_paths == {
|
assert file_paths == {
|
||||||
|
|
@ -86,7 +87,7 @@ def test_error_bad_path():
|
||||||
|
|
||||||
(project.dir / "setup.py").unlink()
|
(project.dir / "setup.py").unlink()
|
||||||
try:
|
try:
|
||||||
list(v1rewrite.iter_file_paths(cfg.file_patterns))
|
list(rewrite.iter_path_patterns_items(cfg.file_patterns))
|
||||||
assert False, "expected IOError"
|
assert False, "expected IOError"
|
||||||
except IOError as ex:
|
except IOError as ex:
|
||||||
assert "setup.py" in str(ex)
|
assert "setup.py" in str(ex)
|
||||||
|
|
@ -102,10 +103,11 @@ def test_error_bad_pattern():
|
||||||
patterns["setup.py"] = patterns["setup.py"][0] + "invalid"
|
patterns["setup.py"] = patterns["setup.py"][0] + "invalid"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
old_vinfo = v1version.parse_version_info("v201808.0233")
|
||||||
new_vinfo = v1version.parse_version_info("v201809.1234")
|
new_vinfo = v1version.parse_version_info("v201809.1234")
|
||||||
list(v1rewrite.diff(new_vinfo, patterns))
|
list(v1rewrite.diff(old_vinfo, new_vinfo, patterns))
|
||||||
assert False, "expected v1rewrite.NoPatternMatch"
|
assert False, "expected rewrite.NoPatternMatch"
|
||||||
except v1rewrite.NoPatternMatch as ex:
|
except rewrite.NoPatternMatch as ex:
|
||||||
assert "setup.py" in str(ex)
|
assert "setup.py" in str(ex)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,10 @@ import datetime as dt
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
import pycalver.version as v1version
|
from pycalver import version
|
||||||
import pycalver2.version as v2version
|
from pycalver import v1version
|
||||||
import pycalver.patterns as v1patterns
|
from pycalver import v2version
|
||||||
|
from pycalver import v1patterns
|
||||||
|
|
||||||
# import pycalver2.patterns as v2patterns
|
# import pycalver2.patterns as v2patterns
|
||||||
|
|
||||||
|
|
@ -51,7 +52,7 @@ def test_bump_random(monkeypatch):
|
||||||
cur_date = dt.date(2016, 1, 1) + dt.timedelta(days=random.randint(1, 2000))
|
cur_date = dt.date(2016, 1, 1) + dt.timedelta(days=random.randint(1, 2000))
|
||||||
cur_version = cur_date.strftime("v%Y%m") + ".0001-dev"
|
cur_version = cur_date.strftime("v%Y%m") + ".0001-dev"
|
||||||
|
|
||||||
monkeypatch.setattr(v1version, 'TODAY', cur_date)
|
monkeypatch.setattr(version, 'TODAY', cur_date)
|
||||||
|
|
||||||
for _ in range(1000):
|
for _ in range(1000):
|
||||||
cur_date += dt.timedelta(days=int((1 + random.random()) ** 10))
|
cur_date += dt.timedelta(days=int((1 + random.random()) ** 10))
|
||||||
|
|
@ -120,7 +121,7 @@ def test_parse_error_empty():
|
||||||
try:
|
try:
|
||||||
v1version.parse_version_info("")
|
v1version.parse_version_info("")
|
||||||
assert False
|
assert False
|
||||||
except v1version.PatternError as err:
|
except version.PatternError as err:
|
||||||
assert "Invalid version string" in str(err)
|
assert "Invalid version string" in str(err)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -128,7 +129,7 @@ def test_parse_error_noprefix():
|
||||||
try:
|
try:
|
||||||
v1version.parse_version_info("201809.0002")
|
v1version.parse_version_info("201809.0002")
|
||||||
assert False
|
assert False
|
||||||
except v1version.PatternError as err:
|
except version.PatternError as err:
|
||||||
assert "Invalid version string" in str(err)
|
assert "Invalid version string" in str(err)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -136,7 +137,7 @@ def test_parse_error_nopadding():
|
||||||
try:
|
try:
|
||||||
v1version.parse_version_info("v201809.2b0")
|
v1version.parse_version_info("v201809.2b0")
|
||||||
assert False
|
assert False
|
||||||
except v1version.PatternError as err:
|
except version.PatternError as err:
|
||||||
assert "Invalid version string" in str(err)
|
assert "Invalid version string" in str(err)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -151,7 +152,7 @@ def test_part_field_mapping_v1():
|
||||||
assert not any(b_extra_names), sorted(b_extra_names)
|
assert not any(b_extra_names), sorted(b_extra_names)
|
||||||
|
|
||||||
a_fields = set(v1patterns.PATTERN_PART_FIELDS.values())
|
a_fields = set(v1patterns.PATTERN_PART_FIELDS.values())
|
||||||
b_fields = set(v1version.VersionInfo._fields)
|
b_fields = set(version.V1VersionInfo._fields)
|
||||||
|
|
||||||
a_extra_fields = a_fields - b_fields
|
a_extra_fields = a_fields - b_fields
|
||||||
b_extra_fields = b_fields - a_fields
|
b_extra_fields = b_fields - a_fields
|
||||||
|
|
@ -200,7 +201,7 @@ def test_v1_parse_versions(pattern_str, line, expected_vinfo):
|
||||||
|
|
||||||
# def test_v2_parse_versions(pattern_str, line, expected_vinfo):
|
# def test_v2_parse_versions(pattern_str, line, expected_vinfo):
|
||||||
def test_v2_parse_versions():
|
def test_v2_parse_versions():
|
||||||
_vnfo = v2version.parse_version_info("v201712.0033", pattern="vYYYY0M.BUILD[-TAG[NUM]]")
|
_vnfo = v2version.parse_version_info("v201712.0033", raw_pattern="vYYYY0M.BUILD[-TAG[NUM]]")
|
||||||
fvals = {'year_y': 2017, 'month': 12, 'bid': "0033"}
|
fvals = {'year_y': 2017, 'month': 12, 'bid': "0033"}
|
||||||
assert _vnfo == v2version._parse_version_info(fvals)
|
assert _vnfo == v2version._parse_version_info(fvals)
|
||||||
|
|
||||||
|
|
@ -217,24 +218,24 @@ def test_make_segments():
|
||||||
|
|
||||||
|
|
||||||
def test_v2_format_version():
|
def test_v2_format_version():
|
||||||
pattern = "vYYYY0M.BUILD[-TAG[NUM]]"
|
version_pattern = "vYYYY0M.BUILD[-TAG[NUM]]"
|
||||||
in_version = "v200701.0033-beta"
|
in_version = "v200701.0033-beta"
|
||||||
|
|
||||||
vinfo = v2version.parse_version_info(in_version, pattern=pattern)
|
vinfo = v2version.parse_version_info(in_version, raw_pattern=version_pattern)
|
||||||
out_version = v2version.format_version(vinfo, pattern=pattern)
|
out_version = v2version.format_version(vinfo, raw_pattern=version_pattern)
|
||||||
assert in_version == out_version
|
assert in_version == out_version
|
||||||
|
|
||||||
result = v2version.format_version(vinfo, pattern="v0Y.BUILD[-TAG]")
|
result = v2version.format_version(vinfo, raw_pattern="v0Y.BUILD[-TAG]")
|
||||||
assert result == "v07.0033-beta"
|
assert result == "v07.0033-beta"
|
||||||
|
|
||||||
result = v2version.format_version(vinfo, pattern="vYY.BLD[-TAG]")
|
result = v2version.format_version(vinfo, raw_pattern="vYY.BLD[-TAG]")
|
||||||
assert result == "v7.33-beta"
|
assert result == "v7.33-beta"
|
||||||
|
|
||||||
result = v2version.format_version(vinfo, pattern="vYY.BLD-TAG")
|
result = v2version.format_version(vinfo, raw_pattern="vYY.BLD-TAG")
|
||||||
assert result == "v7.33-beta"
|
assert result == "v7.33-beta"
|
||||||
|
|
||||||
result = v2version.format_version(vinfo, pattern='__version__ = "YYYY.BUILD[-TAG]"')
|
result = v2version.format_version(vinfo, raw_pattern='__version__ = "YYYY.BUILD[-TAG]"')
|
||||||
assert result == '__version__ = "2007.0033-beta"'
|
assert result == '__version__ = "2007.0033-beta"'
|
||||||
|
|
||||||
result = v2version.format_version(vinfo, pattern='__version__ = "YYYY.BLD"')
|
result = v2version.format_version(vinfo, raw_pattern='__version__ = "YYYY.BLD"')
|
||||||
assert result == '__version__ = "2007.33"'
|
assert result == '__version__ = "2007.33"'
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue