diff --git a/README.md b/README.md index fc47b51..66d6047 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# versipy v0.2.4.post1 +# versipy v0.2.5 ![](pictures/versipy.png) @@ -65,9 +65,9 @@ conda update -c aleg -c anaconda -c bioconda -c conda-forge versipy The following dependencies are required but automatically installed with pip or conda package manager -- colorlog>=4.1.0 -- pyyaml>=5.3.1 -- gitpython>=3.1.9 + - colorlog>=4.1.0 + - pyyaml>=5.3.1 + - gitpython>=3.1.9 ## Usage @@ -179,14 +179,65 @@ deploy: tags: true ``` +### Lists + +In addition to simple string replacement, `versipy` offers some advanced syntax for the support of lists (such as +dependencies or classifiers). This syntax allows to some flexibility in formatting lists depending on the template. + +If a managed variable in `versipy.yaml` contains a list, the template needs to specify the formatting as such: + +`__@{::}`__ + +For example, if `versipy.yaml` contains this entry: + +```yaml +managed_values: + __dependencies__: + - numpy + - pandas + - matplotlib +``` + +And the template for `setup.py` contains the following line: +```python + install_requires=["colorlog>=4.1.0", "pyyaml>=5.3.1", "gitpython>=3.1.9"], +``` + +then versipy will interpret `, ` as the list separator, and `"` as both prefix and suffix. The resulting line will then be: + +```python + install_requires=["numpy", "pandas", "matplotlib"], +``` + +Wheras in `meta.yaml` you might define in the template: +```yaml + run: + - colorlog>=4.1.0 + - pyyaml>=5.3.1 + - gitpython>=3.1.9 +``` + +where `versipy` interprets `\n - ` as the separator, and the prefix and suffix are empty strings. The resulting `meta.yaml` +will then contain: + +```yaml + run: + - numpy + - pandas + - matplotlib +``` + + --- + + ## Classifiers -* Development Status :: 3 - Alpha -* Intended Audience :: Science/Research -* Topic :: Scientific/Engineering :: Bio-Informatics -* License :: OSI Approved :: GNU General Public License v3 (GPLv3) +* Development Status :: 3 - Alpha +* Intended Audience :: Science/Research +* Topic :: Scientific/Engineering :: Bio-Informatics +* License :: OSI Approved :: GNU General Public License v3 (GPLv3) * Programming Language :: Python :: 3 ## citation diff --git a/meta.yaml b/meta.yaml index 3127299..04fee59 100644 --- a/meta.yaml +++ b/meta.yaml @@ -1,4 +1,4 @@ -{% set version = "0.2.4.post1" %} +{% set version = "0.2.5" %} {% set name = "versipy" %} package: diff --git a/setup.py b/setup.py index 89b6d82..00c4a66 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setup( name="versipy", description="Versatile version and medatada managment across the python packaging ecosystem with git integration", - version="0.2.4.post1", + version="0.2.5", long_description=long_description, long_description_content_type="text/markdown", url="https://github.com/a-slide/versipy", @@ -19,8 +19,14 @@ author_email="contact@adrienleger.com", license="GPLv3", python_requires=">=3.6", - classifiers=["Development Status :: 3 - Alpha", "Intended Audience :: Science/Research", "Topic :: Scientific/Engineering :: Bio-Informatics", "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", "Programming Language :: Python :: 3"], - install_requires=["colorlog>=4.1.0", "pyyaml>=5.3.1", "gitpython>=3.1.9"], + classifiers=["Development Status :: 3 - Alpha", + "Intended Audience :: Science/Research", + "Topic :: Scientific/Engineering :: Bio-Informatics", + "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", + "Programming Language :: Python :: 3"], + install_requires=["colorlog>=4.1.0", + "pyyaml>=5.3.1", + "gitpython>=3.1.9"], packages=["versipy"], package_dir={"versipy": "versipy"}, package_data={"versipy": ["templates/*"]}, diff --git a/versipy.yaml b/versipy.yaml index 9411b3a..d21b2a3 100644 --- a/versipy.yaml +++ b/versipy.yaml @@ -1,11 +1,11 @@ version: major: 0 minor: 2 - micro: 4 + micro: 5 a: null b: null rc: null - post: 1 + post: null dev: null managed_values: __package_name__: versipy @@ -19,14 +19,16 @@ managed_values: __package_licence_url__: https://www.gnu.org/licenses/gpl-3.0.en.html __minimal_python__: '>=3.6' __entry_point1__: versipy=versipy.__main__:main - __dependency1__: colorlog>=4.1.0 - __dependency2__: pyyaml>=5.3.1 - __dependency3__: gitpython>=3.1.9 - __classifiers_1__: 'Development Status :: 3 - Alpha' - __classifiers_2__: 'Intended Audience :: Science/Research' - __classifiers_3__: 'Topic :: Scientific/Engineering :: Bio-Informatics' - __classifiers_4__: 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)' - __classifiers_5__: 'Programming Language :: Python :: 3' + __dependencies__: + - colorlog>=4.1.0 + - pyyaml>=5.3.1 + - gitpython>=3.1.9 + __classifiers__: + - 'Development Status :: 3 - Alpha' + - 'Intended Audience :: Science/Research' + - 'Topic :: Scientific/Engineering :: Bio-Informatics' + - 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)' + - 'Programming Language :: Python :: 3' __citation__: Adrien Leger. (2020, October 27). a-slide/versipy 0.2.2 (Version 0.2.2). Zenodo. http://doi.org/10.5281/zenodo.4139248 managed_files: diff --git a/versipy/__init__.py b/versipy/__init__.py index d8b950e..3c42a14 100644 --- a/versipy/__init__.py +++ b/versipy/__init__.py @@ -1,5 +1,5 @@ __name__ = "versipy" -__version__ = "0.2.4.post1" +__version__ = "0.2.5" __description__ = "Versatile version and medatada managment across the python packaging ecosystem with git integration" __url__ = "https://github.com/a-slide/versipy" __licence__ = "GPLv3" diff --git a/versipy/common.py b/versipy/common.py index 8e4fe44..ff2fff1 100644 --- a/versipy/common.py +++ b/versipy/common.py @@ -85,7 +85,7 @@ def choose_option(choices=["y", "n"], message="Choose a valid option"): def get_logger(name=None, verbose=False, quiet=False): """Multilevel colored log using colorlog""" - + # Define conditional color formatter formatter = colorlog.LevelFormatter( fmt={ @@ -104,12 +104,12 @@ def get_logger(name=None, verbose=False, quiet=False): }, reset=True, ) - + # Define logger with custom formatter logging.basicConfig(format="%(message)s") logging.getLogger().handlers[0].setFormatter(formatter) log = logging.getLogger(name) - + # Define logging level depending on verbosity if verbose: log.setLevel(logging.DEBUG) @@ -117,7 +117,7 @@ def get_logger(name=None, verbose=False, quiet=False): log.setLevel(logging.WARNING) else: log.setLevel(logging.INFO) - + return log @@ -150,10 +150,10 @@ def log_list(l, logger, header="", indent="\t"): def doc_func(func): """Parse the function description string""" - + if inspect.isclass(func): func = func.__init__ - + docstr_list = [] for l in inspect.getdoc(func).split("\n"): l = l.strip() @@ -162,17 +162,17 @@ def doc_func(func): break else: docstr_list.append(l) - + return " ".join(docstr_list) def make_arg_dict(func): """Parse the arguments default value, type and doc""" - + # Init method for classes if inspect.isclass(func): func = func.__init__ - + if inspect.isfunction(func) or inspect.ismethod(func): # Parse arguments default values and annotations d = OrderedDict() @@ -188,7 +188,7 @@ def make_arg_dict(func): d[name]["required"] = True else: d[name]["default"] = p.default - + # Parse the docstring in a dict docstr_dict = OrderedDict() lab = None @@ -200,7 +200,7 @@ def make_arg_dict(func): docstr_dict[lab] = [] elif lab: docstr_dict[lab].append(l) - + # Concatenate and copy doc in main dict for name in d.keys(): if name in docstr_dict: @@ -210,12 +210,12 @@ def make_arg_dict(func): def arg_from_docstr(parser, func, arg_name, short_name=None): """Get options corresponding to argument name from docstring and deal with special cases""" - + if short_name: arg_names = ["-{}".format(short_name), "--{}".format(arg_name)] else: arg_names = ["--{}".format(arg_name)] - + arg_dict = make_arg_dict(func)[arg_name] if "help" in arg_dict: if "default" in arg_dict: @@ -225,13 +225,13 @@ def arg_from_docstr(parser, func, arg_name, short_name=None): arg_dict["help"] += " (default: %(default)s)" else: arg_dict["help"] += " (required)" - + if "type" in arg_dict: if arg_dict["type"] == bool: arg_dict["help"] += " [boolean]" else: arg_dict["help"] += " [%(type)s]" - + # Special case for boolean args if arg_dict["type"] == bool: if arg_dict["default"] == False: @@ -240,12 +240,12 @@ def arg_from_docstr(parser, func, arg_name, short_name=None): elif arg_dict["default"] == True: arg_dict["action"] = "store_false" del arg_dict["type"] - + # Special case for lists args elif isinstance(arg_dict["type"], list): arg_dict["nargs"] = "*" arg_dict["type"] = arg_dict["type"][0] - + parser.add_argument(*arg_names, **arg_dict) @@ -259,11 +259,11 @@ def ordered_load_yaml(yaml_fn, Loader=yaml.Loader, **kwargs): # Define custom loader class OrderedLoader(Loader): pass - + def construct_mapping(loader, node): loader.flatten_mapping(node) return OrderedDict(loader.construct_pairs(node)) - + OrderedLoader.add_constructor(yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, construct_mapping) # Try to load file try: @@ -281,12 +281,12 @@ def ordered_dump_yaml(d, yaml_fn, Dumper=yaml.Dumper, **kwargs): # Define custom dumper class OrderedDumper(Dumper): pass - + def _dict_representer(dumper, data): return dumper.represent_mapping(yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, data.items()) - + OrderedDumper.add_representer(OrderedDict, _dict_representer) - + # Try to dump dict to file try: with open(yaml_fn, "w") as yaml_fp: @@ -330,11 +330,11 @@ def get_version_str(d): def get_versipy_yaml(versipy_fn, log): """load end check versipy file""" - + # Try to load YAML file log.debug("Loading versipy YAML file") info_d = ordered_load_yaml(versipy_fn) - + # Check that all fields are there log.debug("Checking file structure") for field in ["version", "managed_values", "managed_files"]: @@ -342,7 +342,7 @@ def get_versipy_yaml(versipy_fn, log): raise ValueError("Missing section '{}' in versipy YAML file".format(field)) if not info_d[field]: raise ValueError("Empty section '{}' in versipy YAML file".format(field)) - + # Verify validity of version string log.debug("Checking version") for field in ["major", "minor", "micro", "a", "b", "rc", "post", "dev"]: @@ -351,7 +351,7 @@ def get_versipy_yaml(versipy_fn, log): version_str = get_version_str(info_d["version"]) if not is_canonical_version(version_str): raise ValueError("Current version {} is not a valid PEP canonical version".format(version_str)) - + return info_d @@ -378,11 +378,11 @@ def increment_version( post=False, dev=False, ): - + # safe increment variable even if None def increment_safe(v): return 1 if v is None else v + 1 - + version_d = copy.deepcopy(version_d) if major: log.debug("Increment major level and reset all lower levels") @@ -392,13 +392,13 @@ def increment_safe(v): log.debug("Increment minor level and reset all lower levels") version_d["minor"] = increment_safe(version_d["minor"]) version_d = reset_version(version_d, levels=["micro", "a", "b", "rc", "post", "dev"]) - + # optional micro version number if micro: log.debug("Increment micro level and reset all lower levels") version_d["micro"] = increment_safe(version_d["micro"]) version_d = reset_version(version_d, levels=["a", "b", "rc", "post", "dev"]) - + # optional release type version if rc: log.debug("Increment rc level and reset all lower levels") @@ -412,7 +412,7 @@ def increment_safe(v): log.debug("Increment a level and reset all lower levels") version_d["a"] = increment_safe(version_d["a"]) version_d = reset_version(version_d, levels=["b", "rc", "post", "dev"]) - + # optional post and dev tags if post: log.debug("Increment post level and reset all lower levels") @@ -420,12 +420,12 @@ def increment_safe(v): if dev: log.debug("Increment dev level and reset all lower levels") version_d["dev"] = increment_safe(version_d["dev"]) - + # sanity check version_str = get_version_str(version_d) if not is_canonical_version(version_str): raise ValueError("Current version {version_str} is not a valid PEP canonical version") - + log_dict(version_d, log.debug, "Updated version values") return version_d @@ -434,7 +434,7 @@ def parse_version_str(version_str, log): """""" if not is_canonical_version(version_str): raise ValueError("Current version {version_str} is not a valid PEP canonical version") - + log.debug("Split version number into a list") alphabet = list(string.ascii_letters) l = [] @@ -452,7 +452,7 @@ def parse_version_str(version_str, log): elif c.isdigit(): s += c l.append(s) - + log.debug("Store list values in version dictionary") version_d = OrderedDict(major=0, minor=None, micro=None, a=None, b=None, rc=None, post=None, dev=None) if l[0].isdigit(): @@ -466,7 +466,7 @@ def parse_version_str(version_str, log): if e.startswith(tag): version_d[tag] = int(e.strip(tag)) break - + log_dict(version_d, log.debug, "Updated version values") return version_d @@ -474,20 +474,20 @@ def parse_version_str(version_str, log): def update_managed_files(info_d, overwrite, dry, log): """""" version_str = get_version_str(info_d["version"]) - + for src_fn, dest_fn in info_d["managed_files"].items(): log.debug("Updating file {}".format(dest_fn)) - + # Bulletproof reading and writing try: src_fp = dest_fp = None - + # Open template file for reading try: src_fp = open(src_fn, "r") except: raise IOError("Cannot read source Template file: {}".format(src_fn)) - + # Open destination file for writing if not dry: try: @@ -501,17 +501,48 @@ def update_managed_files(info_d, overwrite, dry, log): dest_fp = open(dest_fn, "w") except: raise IOError("Cannot write to destination file: {}".format(dest_fn)) - + s = src_fp.read() s = s.replace("__package_version__", version_str) for k, v in info_d["managed_values"].items(): - s = s.replace(k, v) - + if isinstance(v, list): + key_name = k[2:-2] + """ + Let me explain the ugliest regex in the world. Note that curly braces are doubled for escaping + __@{{ Literal + ((?!::)[^}}]*) Not :: and not } -> captured as group 1 (separator) + :: Literal + (((?!{key_name}).)*) Not key_name -> captured as group 2 (prefix) + {key_name} Literally key_name + ([^}}]*) Not curly brace -> captured as group 3 (suffix) + }}__ Literal + + So it looks for a string built like: + __@{::
key_name}__
+                    and will replace it with:
+                    
value1
value2
value3
+                    
+                    The regex works for multiple ocurrences.
+                    
+                    For example, assuming key_name is "dependencies" and our dependencies are "numpy", "meth5" and "pandas",
+                    and it finds something like:
+                    __@{, ::"dependencies"}__
+                    it will be replaced with:
+                    "numpy", "meth5", "pandas"
+                    """
+                    s = re.sub(
+                        f"__@{{((?!::)[^}}]*)::(((?!{key_name}).)*){key_name}([^}}]*)}}__",
+                        "\\1".join([f"\\2{vi}\\3" for vi in v]),
+                        s,
+                    )
+                else:
+                    s = s.replace(k, v)
+            
             if dry:
                 stdout_print(s)
             else:
                 dest_fp.write(s)
-
+        
         finally:
             # Try to close file pointers
             for fp, fn in [[src_fp, src_fn], [dest_fp, dest_fn]]:
@@ -541,7 +572,7 @@ def update_versipy_files(info_d, versipy_fn, versipy_history_fn, comment, overwr
 
 def get_versipy_yaml_template():
     info_d = OrderedDict()
-
+    
     # Version section
     info_d["version"] = OrderedDict()
     info_d["version"]["major"] = 0
@@ -552,7 +583,7 @@ def get_versipy_yaml_template():
     info_d["version"]["rc"] = None
     info_d["version"]["post"] = None
     info_d["version"]["dev"] = None
-
+    
     # Managed values section
     info_d["managed_values"] = OrderedDict()
     info_d["managed_values"]["__package_name__"] = "package name"
@@ -561,22 +592,22 @@ def get_versipy_yaml_template():
     info_d["managed_values"]["__package_licence__"] = "package licence"
     info_d["managed_values"]["__author_name__"] = "author name"
     info_d["managed_values"]["__author_email__"] = "author contact email"
-
+    
     # Managed files section
     info_d["managed_files"] = OrderedDict()
     info_d["managed_files"]["versipy_templates/setup.py"] = "setup.py"
     info_d["managed_files"]["versipy_templates/meta.yaml"] = "meta.yaml"
     info_d["managed_files"]["versipy_templates/__init__.py"] = "versipy/__init__.py"
     info_d["managed_files"]["versipy_templates/README.md"] = "README.md"
-
+    
     return info_d
 
 
 def write_versipy_yaml(versipy_fn, overwrite, log):
-
+    
     info_d = versipy_info_d()
     log_dict(info_d, log.debug, "template info dict")
-
+    
     # Open destination file for writing
     log.debug("Try to dump data to YAML file {}".format(versipy_fn))
     if not overwrite and os.path.isfile(versipy_fn):
@@ -593,18 +624,18 @@ def git_files(files, version, comment, git_tag, log):
         log.debug("Acquire local repository")
         repo = Repo()
         remote = repo.remote("origin")
-
+        
         log.debug("Add, commit and push version files")
         for f in files:
             repo.index.add(f)
         commit = repo.index.commit(message=comment)
         push = remote.push()
-
+        
         if git_tag:
             log.debug("Set and push new version tag")
             tag = repo.create_tag(version, message=comment)
             push = remote.push(tag)
-
+    
     except Exception as E:
         log.info("Failed to push to remote")
         log.debug(type(E), str(E))
diff --git a/versipy_history.txt b/versipy_history.txt
index 0896412..3a25973 100644
--- a/versipy_history.txt
+++ b/versipy_history.txt
@@ -16,3 +16,16 @@
 2020-10-27 16:29:35.809467	0.2.3.dev2	Update doc and formatting changes
 2020-10-27 16:34:43.746265	0.2.4	Update doc and formatting changes
 2020-10-27 18:28:53.873189	0.2.4.post1	typo fix
+2021-05-14 14:33:22.566348	0.2.4.post1.dev1	Versipy auto bump-up
+2021-05-14 14:34:34.599597	0.2.4.post1.dev2	Versipy auto bump-up
+2021-05-14 14:34:59.852766	0.2.4.post1.dev3	Versipy auto bump-up
+2021-05-14 14:41:40.568131	0.2.4.post1.dev4	Versipy auto bump-up
+2021-05-14 14:42:32.820918	0.2.4.post1.dev5	Versipy auto bump-up
+2021-05-14 14:42:55.346348	0.2.4.post1.dev6	Versipy auto bump-up
+2021-05-14 14:43:29.700489	0.2.4.post1.dev7	Versipy auto bump-up
+2021-05-14 14:43:46.163946	0.2.4.post1.dev8	Versipy auto bump-up
+2021-05-14 14:44:38.270605	0.2.4.post1.dev9	Versipy auto bump-up
+2021-05-14 14:46:00.410942	0.2.4.post1.dev10	Versipy auto bump-up
+2021-05-14 14:46:48.540644	0.2.4.post1.dev11	Versipy auto bump-up
+2021-05-14 14:47:48.960540	0.2.5	Versipy auto bump-up
+2021-05-17 11:58:08.278705	0.2.5	Manually set version
diff --git a/versipy_templates/README.md b/versipy_templates/README.md
index 8cb96fd..48dd29e 100644
--- a/versipy_templates/README.md
+++ b/versipy_templates/README.md
@@ -65,9 +65,8 @@ conda update -c aleg -c anaconda -c bioconda -c conda-forge versipy
 
 The following dependencies are required but automatically installed with pip or conda package manager
 
-- __dependency1__
-- __dependency2__
-- __dependency3__
+ - __@{ 
+ - ::dependencies}__
 
 ## Usage
 
@@ -179,15 +178,62 @@ deploy:
       tags: true
 ```
 
+### Lists
+
+In addition to simple string replacement, `versipy` offers some advanced syntax for the support of lists (such as 
+dependencies or classifiers). This syntax allows to some flexibility in formatting lists depending on the template.
+
+If a managed variable in `versipy.yaml` contains a list, the template needs to specify the formatting as such:
+
+`__@{::}`__
+
+For example, if `versipy.yaml` contains this entry:
+
+```yaml
+managed_values:
+  __dependencies__:
+  - numpy
+  - pandas
+  - matplotlib
+```
+
+And the template for `setup.py` contains the following line:
+```python
+    install_requires=[__@{, ::"dependencies"}__],
+```
+
+then versipy will interpret `, ` as the list separator, and `"` as both prefix and suffix. The resulting line will then be:
+
+```python
+    install_requires=["numpy", "pandas", "matplotlib"],
+```
+
+Wheras in `meta.yaml` you might define in the template:
+```yaml
+  run:
+  - __@{
+  - ::dependencies}__
+```
+
+where `versipy` interprets `\n  - ` as the separator, and the prefix and suffix are empty strings. The resulting `meta.yaml`
+will then contain:
+
+```yaml
+  run:
+  - numpy
+  - pandas
+  - matplotlib   
+```
+
+
 ---
 
+
+
 ## Classifiers
 
-* __classifiers_1__
-* __classifiers_2__
-* __classifiers_3__
-* __classifiers_4__
-* __classifiers_5__
+* __@{ 
+* ::classifiers}__
 
 ## citation
 
diff --git a/versipy_templates/meta.yaml b/versipy_templates/meta.yaml
index d5f4683..8d2666f 100644
--- a/versipy_templates/meta.yaml
+++ b/versipy_templates/meta.yaml
@@ -22,9 +22,8 @@ requirements:
     - pip>=19.2.1
     - ripgrep>=11.0.1
   run:
-    - __dependency1__
-    - __dependency2__
-    - __dependency3__
+    - __@{
+    - ::dependencies}__
 about:
   home: __package_url__
   license: __package_licence__
diff --git a/versipy_templates/setup.py b/versipy_templates/setup.py
index 72c3342..4f6a72c 100644
--- a/versipy_templates/setup.py
+++ b/versipy_templates/setup.py
@@ -19,8 +19,10 @@
     author_email="__author_email__",
     license="__package_licence__",
     python_requires="__minimal_python__",
-    classifiers=["__classifiers_1__", "__classifiers_2__", "__classifiers_3__", "__classifiers_4__", "__classifiers_5__"],
-    install_requires=["__dependency1__", "__dependency2__", "__dependency3__"],
+    classifiers=[__@{,
+        ::"classifiers"}__],
+    install_requires=[__@{,
+        ::"dependencies"}__],
     packages=["__package_name__"],
     package_dir={"__package_name__": "__package_name__"},
     package_data={"__package_name__": ["templates/*"]},