diff --git a/.cruft.json b/.cruft.json index 52fee42..3a7dc64 100644 --- a/.cruft.json +++ b/.cruft.json @@ -1,6 +1,6 @@ { "template": "https://github.com/brightway-lca/cookiecutter-brightwaylib", - "commit": "ed6d9090fe8aebb87cd198be5b1abe84e39db280", + "commit": "9ee47efb8072303cf6476d873f392c7753d98082", "checkout": null, "context": { "cookiecutter": { diff --git a/.github/workflows/conda-package-deploy.yaml b/.github/workflows/conda-package-deploy.yaml index 6de4d3c..7d2964c 100644 --- a/.github/workflows/conda-package-deploy.yaml +++ b/.github/workflows/conda-package-deploy.yaml @@ -4,6 +4,7 @@ name: Build and deploy conda packages (bw2ui and bw25ui) on: push: branches: [main, develop] + tags: '*' paths-ignore: # Don't re-build if we only change README.md Or CHANGES.md - '**.md' @@ -22,7 +23,9 @@ jobs: with: python-version: 3.11 auto-activate-base: true - - run: | + - name: Publish distribution 📦 to conda channels + if: startsWith(github.ref, 'refs/tags') + run: | conda info conda config --set auto_update_conda False conda install -y -q conda-build anaconda-client diff --git a/.github/workflows/python-package-deploy-bw25.yml b/.github/workflows/python-package-deploy-bw25.yml index bb717e4..83eb083 100644 --- a/.github/workflows/python-package-deploy-bw25.yml +++ b/.github/workflows/python-package-deploy-bw25.yml @@ -6,6 +6,7 @@ name: Build and publish bw25ui Python 🐍 distributions 📦 to PyPI on: push: branches: [main, develop] + tags: '*' paths-ignore: # Don't re-build if we only change README.md Or CHANGES.md - '**.md' @@ -48,5 +49,5 @@ jobs: #repository-url: https://test.pypi.org/legacy/ #skip-existing: true - name: Publish distribution 📦 to PyPI - #if: startsWith(github.ref, 'refs/tags') + if: startsWith(github.ref, 'refs/tags') uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/python-package-deploy.yml b/.github/workflows/python-package-deploy.yml index ee90338..1010354 100644 --- a/.github/workflows/python-package-deploy.yml +++ b/.github/workflows/python-package-deploy.yml @@ -6,6 +6,7 @@ name: Build and publish bw2ui Python 🐍 distributions 📦 to PyPI on: push: branches: [main, develop] + tags: '*' paths-ignore: # Don't re-build if we only change README.md Or CHANGES.md - '**.md' @@ -45,5 +46,5 @@ jobs: #repository-url: https://test.pypi.org/legacy/ #skip-existing: true - name: Publish distribution 📦 to PyPI - #if: startsWith(github.ref, 'refs/tags') + if: startsWith(github.ref, 'refs/tags') uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.gitignore b/.gitignore index 70bccc9..24f47dc 100644 --- a/.gitignore +++ b/.gitignore @@ -93,7 +93,7 @@ instance/ .scrapy # Sphinx documentation -docs/_build/ +_build/ # PyBuilder target/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5a1b275..899779b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ exclude: '^docs/conf.py' repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v4.6.0 hooks: - id: trailing-whitespace - id: check-added-large-files @@ -42,7 +42,7 @@ repos: args: [--settings-path=pyproject.toml] - repo: https://github.com/psf/black - rev: 24.3.0 + rev: 24.4.2 hooks: - id: black args: [--config=pyproject.toml] diff --git a/CHANGELOG.md b/CHANGES.md similarity index 96% rename from CHANGELOG.md rename to CHANGES.md index 50154ba..cd11040 100644 --- a/CHANGELOG.md +++ b/CHANGES.md @@ -1,5 +1,13 @@ # ui Changelog +## [0.39.0] + ++ support new MultiLCA class API changes + +## [0.38.0] + ++ better handling of text flow for ii text fields + ## [0.37.0] + add option to search by CAS number (only biosphere dbs) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index de3bad9..0fac345 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -78,8 +78,6 @@ $ pytest Unit tests are located in the _tests_ directory, and are written using the [pytest][pytest] testing framework. -[pytest]: https://pytest.readthedocs.io/ - ## How to submit changes Open a [pull request] to submit changes to this project. @@ -109,8 +107,3 @@ This will allow a chance to talk it over with the owners and validate your appro [pytest]: https://pytest.readthedocs.io/ [pull request]: https://github.com/brightway-lca/brightway2-ui/pulls - - - - -[Code of Conduct]: CODE_OF_CONDUCT.md diff --git a/README.md b/README.md index fb39366..010e19f 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,22 @@ # Brightway2-UI +[![PyPI](https://img.shields.io/pypi/v/bw25ui.svg)][pypi status] +[![Status](https://img.shields.io/pypi/status/bw25ui.svg)][pypi status] +[![Python Version](https://img.shields.io/pypi/pyversions/bw25ui)][pypi status] +[![License](https://img.shields.io/pypi/l/bw25ui)][license] + +[![Read the documentation at https://brightway2-ui.readthedocs.io/](https://img.shields.io/readthedocs/brightway2-ui/latest.svg?label=Read%20the%20Docs)][read the docs] +[![Codecov](https://codecov.io/gh/brightway-lca/brightway2-ui/branch/main/graph/badge.svg)][codecov] + +[![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)][pre-commit] +[![Black](https://img.shields.io/badge/code%20style-black-000000.svg)][black] + +[pypi status]: https://pypi.org/project/bw25ui/ +[read the docs]: https://brightway2-ui.readthedocs.io/ +[codecov]: https://app.codecov.io/gh/brightway-lca/brightway2-ui +[pre-commit]: https://github.com/pre-commit/pre-commit +[black]: https://github.com/psf/black + This is now the official repo for Brightway2-UI: > a web and command line user interface, part of the **Brightway2 LCA framework** . @@ -63,3 +80,54 @@ The current main branch will be kept as the branch for development, with identic ### Mid term Once Brightway3 starts to exist, the main branch will be dedicated to it, with a `bw3ui` package. + + +## Contributing + +Contributions are very welcome. +To learn more, see the [Contributor Guide][Contributor Guide]. + +## License + +Distributed under the terms of the [BSD-3 license][License], +_bw2ui_ is free and open source software. + +## Issues + +If you encounter any problems, +please [file an issue][Issue Tracker] along with a detailed description. + + + + +[command-line reference]: https://brightway2-ui.readthedocs.io/en/latest/usage.html +[License]: https://github.com/brightway-lca/brightway2-ui/blob/main/LICENSE +[Contributor Guide]: https://github.com/brightway-lca/brightway2-ui/blob/main/CONTRIBUTING.md +[Issue Tracker]: https://github.com/brightway-lca/brightway2-ui/issues + + +## Building the Documentation + +You can build the documentation locally by installing the documentation Conda environment: + +```bash +conda env create -f docs/environment.yml +``` + +activating the environment + +```bash +conda activate sphinx_brightway2-ui +``` + +and [running the build command](https://www.sphinx-doc.org/en/master/man/sphinx-build.html#sphinx-build): + +```bash +sphinx-build docs _build/html --builder=html --jobs=auto --write-all; open _build/html/index.html +``` + +and [running the build command](https://www.sphinx-doc.org/en/master/man/sphinx-build.html#sphinx-build): + +```bash +sphinx-build docs _build/html --builder=html --jobs=auto --write-all; open _build/html/index.html +``` diff --git a/bw2ui/__init__.py b/bw2ui/__init__.py index afb5b29..3e1326f 100644 --- a/bw2ui/__init__.py +++ b/bw2ui/__init__.py @@ -5,4 +5,4 @@ # Add functions and variables you want exposed in `bw2ui.` namespace here ) -__version__ = "0.37.0" +__version__ = "0.39.0" diff --git a/bw2ui/bin/bw2_browser.py b/bw2ui/bin/bw2_browser.py index b27e782..7673140 100644 --- a/bw2ui/bin/bw2_browser.py +++ b/bw2ui/bin/bw2_browser.py @@ -32,9 +32,10 @@ import uuid import warnings import webbrowser +from packaging import version import bw2analyzer as bwa -from bw2calc import MultiLCA +import bw2calc as bc from bw2data import ( Database, Method, @@ -45,6 +46,9 @@ methods, projects, ) + +if bc.__version__ and isinstance(bc.__version__, str) and version.parse(bc.__version__) >= version.parse("2.0.DEV10"): + from bw2data import get_multilca_data_objs from bw2data.parameters import ( ActivityParameter, DatabaseParameter, @@ -192,6 +196,9 @@ def choose_option(self, opt): index = int(opt) if index >= len(self.current_options.get("formatted", [])): print("There aren't this many options") + elif self.current_options["type"] == "method_namespaces": + self.choose_method_namespace(self.current_options["options"][index]) + elif self.current_options["type"] == "methods": self.choose_method(self.current_options["options"][index]) @@ -618,18 +625,51 @@ def list_methods(self): for m in methods_: m_names.add(m[0]) m_names = sorted(m_names) - self.set_current_options( - { - "type": "methods", - "options": list(m_names), - "formatted": ["%(name)s" % {"name": name} for name in m_names], - } - ) - self.print_current_options("Methods") + if has_namespaced_methods(): + self.set_current_options( + { + "type": "method_namespaces", + "options": list(m_names), + "formatted": ["%(name)s" % {"name": name} for name in m_names], + } + ) + self.print_current_options("Method namespaces") + else: + self.set_current_options( + { + "type": "methods", + "options": list(m_names), + "formatted": ["%(name)s" % {"name": name} for name in m_names], + } + ) + self.print_current_options("Methods") else: self.set_current_options(None) self.update_prompt() + def choose_method_namespace(self, method_namespace): + self.method_namespace = method_namespace + self.method = self.category = self.subcategory = None + self.history.append(("method_namespace", method_namespace)) + if self.autosave: + config.p["ab_method_namespace"] = self.method + config.p["ab_history"] = self.history[-10:] + config.save_preferences() + c_names = set([]) + methods_ = sorted(methods) + for m in [m for m in methods_ if m[0] == method_namespace]: + c_names.add(m[1]) + c_names = sorted(c_names) + self.set_current_options( + { + "type": "methods", + "options": list(c_names), + "formatted": ["%(name)s" % {"name": name} for name in c_names], + } + ) + self.print_current_options("Methods") + self.update_prompt() + def choose_method(self, method): self.method = method self.category = self.subcategory = None @@ -640,8 +680,14 @@ def choose_method(self, method): config.save_preferences() c_names = set([]) methods_ = sorted(methods) - for m in [m for m in methods_ if m[0] == method]: - c_names.add(m[1]) + if has_namespaced_methods(): + for m in [ + m for m in methods_ if m[0] == self.method_namespace and m[1] == method + ]: + c_names.add(m[2]) + else: + for m in [m for m in methods_ if m[0] == method]: + c_names.add(m[1]) c_names = sorted(c_names) self.set_current_options( { @@ -662,8 +708,18 @@ def choose_category(self, category): config.save_preferences() c_names = set([]) methods_ = sorted(methods) - for m in [m for m in methods_ if m[0] == self.method and m[1] == category]: - c_names.add(m[2]) + if has_namespaced_methods(): + for m in [ + m + for m in methods_ + if m[0] == self.method_namespace + and m[1] == self.method + and m[2] == category + ]: + c_names.add(m[3]) + else: + for m in [m for m in methods_ if m[0] == self.method and m[1] == category]: + c_names.add(m[2]) self.set_current_options( { "type": "subcategories", @@ -979,6 +1035,8 @@ def do_ii(self, arg): ) indentation_char = " " * 4 line_length = 50 # TODO: use dynamic line lenght or take from prefs + t_wrapper = textwrap.TextWrapper() + t_wrapper.width = line_length for field in [ k for k in ds.keys() @@ -994,12 +1052,38 @@ def do_ii(self, arg): "code", ] ]: - field_contents = textwrap.wrap(repr(ds[field]), width=line_length) - print("%(tab)s%(field)s:" % {"tab": indentation_char, "field": field}) - for line in field_contents: + if field.casefold() == "comment".casefold(): + t_wrapper.replace_whitespace = False + contents = "\n".join( + [ + "\n".join(t_wrapper.wrap(line)) + for line in ds[field].splitlines() + if line.strip() != "" + ] + ) print( - "%(tab)s%(line)s" % {"tab": indentation_char * 2, "line": line} + "%(tab)s%(field)s:" % {"tab": indentation_char, "field": field} ) + for line in contents.splitlines(): + print( + "%(tab)s%(line)s" + % {"tab": indentation_char * 2, "line": line} + ) + else: + if isinstance(ds[field], str): + t_wrapper.replace_whitespace = False + field_contents = t_wrapper.wrap(ds[field]) + else: + t_wrapper.break_long_words = False + field_contents = t_wrapper.wrap(repr(ds[field])) + print( + "%(tab)s%(field)s:" % {"tab": indentation_char, "field": field} + ) + for line in field_contents: + print( + "%(tab)s%(line)s" + % {"tab": indentation_char * 2, "line": line} + ) def do_l(self, arg): """List current options""" @@ -1255,11 +1339,31 @@ def do_wh(self, arg): f.write(line + "\n") print("History exported to %(fp)s" % {"fp": fp}) - def do_G(self, arg): - """Do an LCIA of the selected activity + method[s]""" - if self.activity and self.method: - method_key_list = [] - + def build_method_key_list(self): + method_key_list = [] + if has_namespaced_methods(): + if ( + self.method_namespace + and self.method + and self.category + and self.subcategory + ): + method_id = ( + self.method_namespace, + self.method, + self.category, + self.subcategory, + ) + method_key_list.append(method_id) + elif self.method_namespace and self.method and self.category is None: + for m in methods: + if m[0] == self.method_namespace and m[1] == self.method: + method_key_list.append(m) + elif self.method_namespace and self.method is None: + for m in methods: + if m[0] == self.method_namespace: + method_key_list.append(m) + else: if self.method and self.category and self.subcategory: method_id = (self.method, self.category, self.subcategory) method_key_list.append(method_id) @@ -1271,23 +1375,66 @@ def do_G(self, arg): for m in methods: if m[0] == self.method: method_key_list.append(m) - bw2browser_cs = { - "inv": [{get_activity(self.activity): 1}], - "ia": method_key_list, - } - tmp_cs_id = uuid.uuid1() - calculation_setups[str(tmp_cs_id)] = bw2browser_cs - mlca = MultiLCA(str(tmp_cs_id)) - formatted_res = [ - [ - mlca.methods[i][0], - mlca.methods[i][1], - mlca.methods[i][2], - Method(mlca.methods[i]).metadata["unit"], - score.pop(), + return method_key_list + + def do_G(self, arg): + """Do an LCIA of the selected activity + method[s]""" + if self.activity and self.method: + method_key_list = self.build_method_key_list() + + if has_namespaced_methods(): + namespace_shift = 1 + else: + namespace_shift = 0 + + if bc.__version__ and isinstance(bc.__version__, str) and version.parse(bc.__version__) >= version.parse( + "2.0.DEV10" + ): + # the configuration + config = {"impact_categories": method_key_list} + activities = [get_activity(self.activity)] + func_units = {a["name"]: {a.id: 1.0} for a in activities} + data_objs = get_multilca_data_objs( + functional_units=func_units, method_config=config + ) + mlca = bc.MultiLCA( + demands=func_units, method_config=config, data_objs=data_objs + ) + mlca.lci() + mlca.lcia() + formatted_res = [] + for (method, fu), score in mlca.scores.items(): + method_name = method[0 + namespace_shift] + category_name = method[1 + namespace_shift] + indicator_name = method[2 + namespace_shift] + formatted_res.append( + [ + method_name, + category_name, + indicator_name, + Method(method).metadata["unit"], + score, + ] + ) + + else: + bw2browser_cs = { + "inv": [{get_activity(self.activity): 1}], + "ia": method_key_list, + } + tmp_cs_id = uuid.uuid1() + calculation_setups[str(tmp_cs_id)] = bw2browser_cs + mlca = bc.MultiLCA(str(tmp_cs_id)) + formatted_res = [ + [ + mlca.methods[i][0], + mlca.methods[i][1], + mlca.methods[i][2], + Method(mlca.methods[i]).metadata["unit"], + score.pop(), + ] + for i, score in enumerate(mlca.results.T.tolist()) ] - for i, score in enumerate(mlca.results.T.tolist()) - ] self.tabulate_data = tabulate( formatted_res, headers=["method", "category", "subcategory", "unit", "score"], @@ -1579,6 +1726,13 @@ def bw2_compat_annotated_top_emissions(lca, names=True, **kwargs): def is_legacy_bwa(): return bwa.__version__[0] == 0 and bwa.__version__[1] == 10 +def is_legacy_bc(): + return isinstance(bc.__version__, tuple) + + +def has_namespaced_methods(): + return len(list(methods)[0]) == 4 + def main(): arguments = docopt(__doc__, version="Brightway2 Activity Browser 2.0") diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..093ce0c --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,118 @@ +# conf.py +# Sphinx configuration file +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +### import setup ################################################################################## + +import datetime + +### project information ########################################################################### + +project = "brightway2-ui" +author = "Brightway Developers" +copyright = datetime.date.today().strftime("%Y") + ' Brightway Developers' + +### project configuration ######################################################################### + +extensions = [ + # native extensions + 'sphinx.ext.autodoc', + 'sphinx.ext.autosummary', + 'sphinx.ext.mathjax', + 'sphinx.ext.viewcode', + 'sphinx.ext.intersphinx', + 'sphinx.ext.extlinks', + # theme + 'sphinx_rtd_theme', + # Markdown support + 'myst_parser', + # API documentation support + 'autoapi', + # responsive web component support + 'sphinx_design', + # copy button on code blocks + "sphinx_copybutton", +] + +exclude_patterns = ['_build'] + +# The master toctree document. +master_doc = 'index' + +### intersphinx configuration ###################################################################### + +intersphinx_mapping = { + "bw": ("https://docs.brightway.dev/en/latest/", None), +} + +### theme configuration ############################################################################ + +html_theme = "sphinx_rtd_theme" +html_title = "brightway2-ui" +html_show_sphinx = False + +html_theme_options = { + 'logo_only': False, + 'display_version': True, + 'prev_next_buttons_location': 'bottom', + # Toc options + 'collapse_navigation': True, + 'sticky_navigation': True, + 'navigation_depth': 4, + 'includehidden': True, + 'titles_only': False +} + +html_logo = 'https://raw.githubusercontent.com/brightway-lca/brightway-documentation/main/source/_static/logo/BW_all_white_transparent_landscape_wide.svg' +html_favicon = 'https://github.com/brightway-lca/brightway-documentation/blob/main/source/_static/logo/BW_favicon_500x500.png' + +### extension configuration ######################################################################## + +## myst_parser configuration ############################################ +## https://myst-parser.readthedocs.io/en/latest/configuration.html + +source_suffix = { + '.rst': 'restructuredtext', + '.md': 'markdown', +} + +myst_enable_extensions = [ + "amsmath", + "colon_fence", + "deflist", + "dollarmath", + "html_image", +] + +## autoapi configuration ################################################ +## https://sphinx-autoapi.readthedocs.io/en/latest/reference/config.html#customisation-options + +autoapi_options = [ + 'members', + 'undoc-members', + 'private-members', + 'show-inheritance', + 'show-module-summary', +] + +autoapi_python_class_content = 'both' +autoapi_member_order = 'groupwise' +autoapi_root = 'content/api' +autoapi_keep_files = False + +autoapi_dirs = [ + '../bw2ui', +] + +autoapi_ignore = [ + '*/data/*', + '*tests/*', + '*tests.py', + '*validation.py', + '*version.py', + '*.rst', + '*.yml', + '*.md', + '*.json', + '*.data' +] diff --git a/docs/content/changelog.md b/docs/content/changelog.md new file mode 100644 index 0000000..8b1749d --- /dev/null +++ b/docs/content/changelog.md @@ -0,0 +1,2 @@ +```{include} ../../CHANGES.md +``` diff --git a/docs/content/codeofconduct.md b/docs/content/codeofconduct.md new file mode 100644 index 0000000..cc6912b --- /dev/null +++ b/docs/content/codeofconduct.md @@ -0,0 +1,2 @@ +```{include} ../../CODE_OF_CONDUCT.md +``` diff --git a/docs/content/contributing.md b/docs/content/contributing.md new file mode 100644 index 0000000..b2d2646 --- /dev/null +++ b/docs/content/contributing.md @@ -0,0 +1 @@ +```{include} ../../CONTRIBUTING.md diff --git a/docs/content/license.md b/docs/content/license.md new file mode 100644 index 0000000..00eca8b --- /dev/null +++ b/docs/content/license.md @@ -0,0 +1,7 @@ +# License + +```{literalinclude} ../../LICENSE +--- +language: none +--- +``` diff --git a/docs/content/usage.md b/docs/content/usage.md new file mode 100644 index 0000000..8f04b05 --- /dev/null +++ b/docs/content/usage.md @@ -0,0 +1 @@ +# Usage diff --git a/docs/environment.yaml b/docs/environment.yaml new file mode 100644 index 0000000..706544d --- /dev/null +++ b/docs/environment.yaml @@ -0,0 +1,25 @@ +# environment.yaml +# Conda environment file for Read the Docs build of the Sphinx project +# https://conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html#creating-an-environment-file-manually + +name: sphinx_brightway2-ui +channels: + - conda-forge + - nodefaults +dependencies: + # core functionality + - python + - ipython + # sphinx + - sphinx # core builder # https://anaconda.org/conda-forge/sphinx/files + # them + - sphinx_rtd_theme # theme # https://anaconda.org/conda-forge/sphinx_rtd_theme/files + # extensions + - myst-parser # Markdown support # https://anaconda.org/conda-forge/myst-parser/files + - sphinx-design # responsive web component support # https://anaconda.org/conda-forge/sphinx-design/files + - sphinx-copybutton # for copy button in code blocks # https://anaconda.org/conda-forge/sphinx-copybutton/files + - sphinx-autoapi # to build docs from source code instead of package import # https://anaconda.org/conda-forge/sphinx-autoapi/files + - sphinx-notfound-page # custom 404 page # https://anaconda.org/conda-forge/sphinx-notfound-page/files + - sphinx-copybutton # for copy button in code blocks # https://anaconda.org/conda-forge/sphinx-copybutton/files + # build process + - sphinx-autobuild # live-html support # https://anaconda.org/conda-forge/sphinx-autobuild/files diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..59bb48f --- /dev/null +++ b/docs/index.md @@ -0,0 +1,21 @@ +# brightway2-ui + +```{button-link} https://docs.brightway.dev +:color: info +:expand: +{octicon}`light-bulb;1em` brightway2-ui is a specialized package of the Brightway Software Framework +``` + +```{toctree} +--- +hidden: +maxdepth: 1 +--- +self +content/usage +content/api/index +content/codeofconduct +content/contributing +content/license +content/changelog +```