pax_global_header00006660000000000000000000000064146516621050014520gustar00rootroot0000000000000052 comment=bd5e1b7736f406cdffaef08c593761c46b5f8aab monty-2024.7.29/000077500000000000000000000000001465166210500132155ustar00rootroot00000000000000monty-2024.7.29/.codacy.yml000066400000000000000000000000551465166210500152600ustar00rootroot00000000000000--- exclude_paths: - tests/** - docs*/** monty-2024.7.29/.github/000077500000000000000000000000001465166210500145555ustar00rootroot00000000000000monty-2024.7.29/.github/dependabot.yml000066400000000000000000000004521465166210500174060ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: pip directory: "/" schedule: interval: daily time: "13:00" open-pull-requests-limit: 10 ignore: - dependency-name: pylint versions: - 2.7.0 - 2.7.1 - 2.8.1 - dependency-name: numpy versions: - 1.19.5 - 1.20.0 monty-2024.7.29/.github/workflows/000077500000000000000000000000001465166210500166125ustar00rootroot00000000000000monty-2024.7.29/.github/workflows/lint.yml000066400000000000000000000010541465166210500203030ustar00rootroot00000000000000name: Linting on: [push, pull_request] jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v4 with: python-version: "3.x" - name: Install dependencies run: | pip install --upgrade ruff mypy - name: ruff run: | ruff --version ruff check . ruff format --check . - name: mypy run: | mypy --version rm -rf .mypy_cache mypy src monty-2024.7.29/.github/workflows/release.yml000066400000000000000000000015141465166210500207560ustar00rootroot00000000000000name: Lint, Test, Release on: release: types: [created] workflow_dispatch: jobs: test: # run test.yml first to ensure that the test suite is passing uses: ./.github/workflows/test.yml build: needs: test strategy: max-parallel: 3 matrix: os: [macos-14] python-version: ["3.x"] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: release env: TWINE_USERNAME: ${{ secrets.TWINE_USERNAME }} TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }} run: | pip install build twine python -m build twine upload --skip-existing dist/* monty-2024.7.29/.github/workflows/test.yml000066400000000000000000000015071465166210500203170ustar00rootroot00000000000000name: Testing on: [push, pull_request, workflow_call] jobs: build: strategy: max-parallel: 20 matrix: os: [ubuntu-latest, macos-14, windows-latest] python-version: ["3.9", "3.x"] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install -e '.[ci]' - name: pytest run: pytest --cov=monty --cov-report html:coverage_reports tests - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v3 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} monty-2024.7.29/.gitignore000066400000000000000000000006051465166210500152060ustar00rootroot00000000000000*.py[cod] # C extensions *.so # Packages *.egg *.egg-info dist build _build _site eggs parts bin var sdist develop-eggs .installed.cfg lib lib64 __pycache__ # Installer logs pip-log.txt # Unit test / coverage reports .coverage .tox .cache nosetests.xml # Translations *.mo # Mr Developer .mr.developer.cfg .project .pydevproject .idea .mypy_cache # vim files *.swp *.swo .DS_Store monty-2024.7.29/.pre-commit-config.yaml000066400000000000000000000031041465166210500174740ustar00rootroot00000000000000exclude: ^(docs|tests/files|cmd_line|tasks.py) ci: autoupdate_schedule: monthly skip: [mypy] autofix_commit_msg: pre-commit auto-fixes autoupdate_commit_msg: pre-commit autoupdate repos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.5.0 hooks: - id: ruff args: [--fix] - id: ruff-format - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.6.0 hooks: - id: check-yaml - id: end-of-file-fixer exclude: ^tests - id: trailing-whitespace exclude: ^tests - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.10.1 hooks: - id: mypy - repo: https://github.com/codespell-project/codespell rev: v2.3.0 hooks: - id: codespell stages: [commit, commit-msg] exclude_types: [html] additional_dependencies: [tomli] # needed to read pyproject.toml below py3.11 - repo: https://github.com/MarcoGorelli/cython-lint rev: v0.16.2 hooks: - id: cython-lint args: [--no-pycodestyle] - id: double-quote-cython-strings - repo: https://github.com/adamchainz/blacken-docs rev: 1.18.0 hooks: - id: blacken-docs - repo: https://github.com/igorshubovych/markdownlint-cli rev: v0.41.0 hooks: - id: markdownlint # MD013: line too long # MD024: Multiple headings with the same content # MD033: no inline HTML # MD041: first line in a file should be a top-level heading # MD025: single title args: [--disable, MD013, MD024, MD025, MD033, MD041, "--"] monty-2024.7.29/LICENSE.rst000066400000000000000000000021001465166210500150220ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2014 Materials Virtual Lab Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. monty-2024.7.29/MANIFEST.in000066400000000000000000000001021465166210500147440ustar00rootroot00000000000000include *.md LICENSE.rst recursive-include monty *.py prune tests monty-2024.7.29/README.md000066400000000000000000000035631465166210500145030ustar00rootroot00000000000000[![GitHub license](https://img.shields.io/github/license/materialsvirtuallab/monty)](https://github.com/materialsvirtuallab/monty/blob/main/LICENSE) [![Linting](https://github.com/materialsvirtuallab/monty/workflows/Linting/badge.svg)](https://github.com/materialsvirtuallab/monty/workflows/Linting/badge.svg) [![Testing](https://github.com/materialsvirtuallab/monty/workflows/Testing/badge.svg)](https://github.com/materialsvirtuallab/monty/workflows/Testing/badge.svg) [![Downloads](https://static.pepy.tech/badge/monty)](https://pepy.tech/project/monty) [![codecov](https://codecov.io/gh/materialsvirtuallab/monty/branch/master/graph/badge.svg?token=QdfT2itxgu)](https://codecov.io/gh/materialsvirtuallab/monty) # Monty: Python Made Even Easier Monty is the missing complement to Python. Monty implements supplementary useful functions for Python that are not part of the standard library. Examples include useful utilities like transparent support for zipped files, useful design patterns such as singleton and cached_class, and many more. Python is a great programming language and comes with "batteries included". However, even Python has missing functionality and/or quirks that make it more difficult to do many simple tasks. In the process of creating several large scientific frameworks based on Python, my co-developers and I have found that it is often useful to create reusable utility functions to supplement the Python standard library. Our forays in various developer sites and forums also found that many developers are looking for solutions to the same problems. Monty is created to serve as a complement to the Python standard library. It provides suite of tools to solve many common problems, and hopefully, be a resource to collect the best solutions. Monty supports Python 3.x. Please visit the [official docs](https://materialsvirtuallab.github.io/monty) for more information. monty-2024.7.29/docs/000077500000000000000000000000001465166210500141455ustar00rootroot00000000000000monty-2024.7.29/docs/Gemfile000066400000000000000000000024661465166210500154500ustar00rootroot00000000000000source "https://rubygems.org" # Hello! This is where you manage which Jekyll version is used to run. # When you want to use a different version, change it below, save the # file and run `bundle install`. Run Jekyll with `bundle exec`, like so: # # bundle exec jekyll serve # # This will help ensure the proper Jekyll version is running. # Happy Jekylling! # gem "jekyll", "~> 4.3.2" # This is the default theme for new Jekyll sites. You may change this to anything you like. gem "minima", "~> 2.5" # If you want to use GitHub Pages, remove the "gem "jekyll"" above and # uncomment the line below. To upgrade, run `bundle update github-pages`. gem "github-pages", group: :jekyll_plugins # If you have any plugins, put them here! group :jekyll_plugins do gem "jekyll-feed", "~> 0.12" end # Windows and JRuby does not include zoneinfo files, so bundle the tzinfo-data gem # and associated library. platforms :mingw, :x64_mingw, :mswin, :jruby do gem "tzinfo", ">= 1", "< 3" gem "tzinfo-data" end # Performance-booster for watching directories on Windows gem "wdm", "~> 0.1.1", :platforms => [:mingw, :x64_mingw, :mswin] # Lock `http_parser.rb` gem to `v0.6.x` on JRuby builds since newer versions of the gem # do not have a Java counterpart. gem "http_parser.rb", "~> 0.6.0", :platforms => [:jruby] gem "webrick", "~> 1.8" monty-2024.7.29/docs/Gemfile.lock000066400000000000000000000165231465166210500163760ustar00rootroot00000000000000GEM remote: https://rubygems.org/ specs: activesupport (7.0.7.2) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) tzinfo (~> 2.0) addressable (2.8.4) public_suffix (>= 2.0.2, < 6.0) coffee-script (2.4.1) coffee-script-source execjs coffee-script-source (1.11.1) colorator (1.1.0) commonmarker (0.23.10) concurrent-ruby (1.2.2) dnsruby (1.70.0) simpleidn (~> 0.2.1) em-websocket (0.5.3) eventmachine (>= 0.12.9) http_parser.rb (~> 0) ethon (0.16.0) ffi (>= 1.15.0) eventmachine (1.2.7) execjs (2.8.1) faraday (2.7.10) faraday-net_http (>= 2.0, < 3.1) ruby2_keywords (>= 0.0.4) faraday-net_http (3.0.2) ffi (1.15.5) forwardable-extended (2.6.0) gemoji (3.0.1) github-pages (228) github-pages-health-check (= 1.17.9) jekyll (= 3.9.3) jekyll-avatar (= 0.7.0) jekyll-coffeescript (= 1.1.1) jekyll-commonmark-ghpages (= 0.4.0) jekyll-default-layout (= 0.1.4) jekyll-feed (= 0.15.1) jekyll-gist (= 1.5.0) jekyll-github-metadata (= 2.13.0) jekyll-include-cache (= 0.2.1) jekyll-mentions (= 1.6.0) jekyll-optional-front-matter (= 0.3.2) jekyll-paginate (= 1.1.0) jekyll-readme-index (= 0.3.0) jekyll-redirect-from (= 0.16.0) jekyll-relative-links (= 0.6.1) jekyll-remote-theme (= 0.4.3) jekyll-sass-converter (= 1.5.2) jekyll-seo-tag (= 2.8.0) jekyll-sitemap (= 1.4.0) jekyll-swiss (= 1.0.0) jekyll-theme-architect (= 0.2.0) jekyll-theme-cayman (= 0.2.0) jekyll-theme-dinky (= 0.2.0) jekyll-theme-hacker (= 0.2.0) jekyll-theme-leap-day (= 0.2.0) jekyll-theme-merlot (= 0.2.0) jekyll-theme-midnight (= 0.2.0) jekyll-theme-minimal (= 0.2.0) jekyll-theme-modernist (= 0.2.0) jekyll-theme-primer (= 0.6.0) jekyll-theme-slate (= 0.2.0) jekyll-theme-tactile (= 0.2.0) jekyll-theme-time-machine (= 0.2.0) jekyll-titles-from-headings (= 0.5.3) jemoji (= 0.12.0) kramdown (= 2.3.2) kramdown-parser-gfm (= 1.1.0) liquid (= 4.0.4) mercenary (~> 0.3) minima (= 2.5.1) nokogiri (>= 1.13.6, < 2.0) rouge (= 3.26.0) terminal-table (~> 1.4) github-pages-health-check (1.17.9) addressable (~> 2.3) dnsruby (~> 1.60) octokit (~> 4.0) public_suffix (>= 3.0, < 5.0) typhoeus (~> 1.3) html-pipeline (2.14.3) activesupport (>= 2) nokogiri (>= 1.4) http_parser.rb (0.8.0) i18n (1.14.1) concurrent-ruby (~> 1.0) jekyll (3.9.3) addressable (~> 2.4) colorator (~> 1.0) em-websocket (~> 0.5) i18n (>= 0.7, < 2) jekyll-sass-converter (~> 1.0) jekyll-watch (~> 2.0) kramdown (>= 1.17, < 3) liquid (~> 4.0) mercenary (~> 0.3.3) pathutil (~> 0.9) rouge (>= 1.7, < 4) safe_yaml (~> 1.0) jekyll-avatar (0.7.0) jekyll (>= 3.0, < 5.0) jekyll-coffeescript (1.1.1) coffee-script (~> 2.2) coffee-script-source (~> 1.11.1) jekyll-commonmark (1.4.0) commonmarker (~> 0.22) jekyll-commonmark-ghpages (0.4.0) commonmarker (~> 0.23.7) jekyll (~> 3.9.0) jekyll-commonmark (~> 1.4.0) rouge (>= 2.0, < 5.0) jekyll-default-layout (0.1.4) jekyll (~> 3.0) jekyll-feed (0.15.1) jekyll (>= 3.7, < 5.0) jekyll-gist (1.5.0) octokit (~> 4.2) jekyll-github-metadata (2.13.0) jekyll (>= 3.4, < 5.0) octokit (~> 4.0, != 4.4.0) jekyll-include-cache (0.2.1) jekyll (>= 3.7, < 5.0) jekyll-mentions (1.6.0) html-pipeline (~> 2.3) jekyll (>= 3.7, < 5.0) jekyll-optional-front-matter (0.3.2) jekyll (>= 3.0, < 5.0) jekyll-paginate (1.1.0) jekyll-readme-index (0.3.0) jekyll (>= 3.0, < 5.0) jekyll-redirect-from (0.16.0) jekyll (>= 3.3, < 5.0) jekyll-relative-links (0.6.1) jekyll (>= 3.3, < 5.0) jekyll-remote-theme (0.4.3) addressable (~> 2.0) jekyll (>= 3.5, < 5.0) jekyll-sass-converter (>= 1.0, <= 3.0.0, != 2.0.0) rubyzip (>= 1.3.0, < 3.0) jekyll-sass-converter (1.5.2) sass (~> 3.4) jekyll-seo-tag (2.8.0) jekyll (>= 3.8, < 5.0) jekyll-sitemap (1.4.0) jekyll (>= 3.7, < 5.0) jekyll-swiss (1.0.0) jekyll-theme-architect (0.2.0) jekyll (> 3.5, < 5.0) jekyll-seo-tag (~> 2.0) jekyll-theme-cayman (0.2.0) jekyll (> 3.5, < 5.0) jekyll-seo-tag (~> 2.0) jekyll-theme-dinky (0.2.0) jekyll (> 3.5, < 5.0) jekyll-seo-tag (~> 2.0) jekyll-theme-hacker (0.2.0) jekyll (> 3.5, < 5.0) jekyll-seo-tag (~> 2.0) jekyll-theme-leap-day (0.2.0) jekyll (> 3.5, < 5.0) jekyll-seo-tag (~> 2.0) jekyll-theme-merlot (0.2.0) jekyll (> 3.5, < 5.0) jekyll-seo-tag (~> 2.0) jekyll-theme-midnight (0.2.0) jekyll (> 3.5, < 5.0) jekyll-seo-tag (~> 2.0) jekyll-theme-minimal (0.2.0) jekyll (> 3.5, < 5.0) jekyll-seo-tag (~> 2.0) jekyll-theme-modernist (0.2.0) jekyll (> 3.5, < 5.0) jekyll-seo-tag (~> 2.0) jekyll-theme-primer (0.6.0) jekyll (> 3.5, < 5.0) jekyll-github-metadata (~> 2.9) jekyll-seo-tag (~> 2.0) jekyll-theme-slate (0.2.0) jekyll (> 3.5, < 5.0) jekyll-seo-tag (~> 2.0) jekyll-theme-tactile (0.2.0) jekyll (> 3.5, < 5.0) jekyll-seo-tag (~> 2.0) jekyll-theme-time-machine (0.2.0) jekyll (> 3.5, < 5.0) jekyll-seo-tag (~> 2.0) jekyll-titles-from-headings (0.5.3) jekyll (>= 3.3, < 5.0) jekyll-watch (2.2.1) listen (~> 3.0) jemoji (0.12.0) gemoji (~> 3.0) html-pipeline (~> 2.2) jekyll (>= 3.0, < 5.0) kramdown (2.3.2) rexml kramdown-parser-gfm (1.1.0) kramdown (~> 2.0) liquid (4.0.4) listen (3.8.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) mercenary (0.3.6) minima (2.5.1) jekyll (>= 3.5, < 5.0) jekyll-feed (~> 0.9) jekyll-seo-tag (~> 2.1) minitest (5.19.0) nokogiri (1.16.5-arm64-darwin) racc (~> 1.4) nokogiri (1.16.5-x86_64-linux) racc (~> 1.4) octokit (4.25.1) faraday (>= 1, < 3) sawyer (~> 0.9) pathutil (0.16.2) forwardable-extended (~> 2.6) public_suffix (4.0.7) racc (1.7.3) rb-fsevent (0.11.2) rb-inotify (0.10.1) ffi (~> 1.0) rexml (3.3.2) strscan rouge (3.26.0) ruby2_keywords (0.0.5) rubyzip (2.3.2) safe_yaml (1.0.5) sass (3.7.4) sass-listen (~> 4.0.0) sass-listen (4.0.0) rb-fsevent (~> 0.9, >= 0.9.4) rb-inotify (~> 0.9, >= 0.9.7) sawyer (0.9.2) addressable (>= 2.3.5) faraday (>= 0.17.3, < 3) simpleidn (0.2.1) unf (~> 0.1.4) strscan (3.1.0) terminal-table (1.8.0) unicode-display_width (~> 1.1, >= 1.1.1) typhoeus (1.4.0) ethon (>= 0.9.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) unf (0.1.4) unf_ext unf_ext (0.0.8.2) unicode-display_width (1.8.0) webrick (1.8.1) PLATFORMS arm64-darwin-22 x86_64-linux DEPENDENCIES github-pages http_parser.rb (~> 0.6.0) jekyll-feed (~> 0.12) minima (~> 2.5) tzinfo (>= 1, < 3) tzinfo-data wdm (~> 0.1.1) webrick (~> 1.8) BUNDLED WITH 2.4.17 monty-2024.7.29/docs/Makefile000066400000000000000000000127051465166210500156120ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = ../docs # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/pymatgen.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/pymatgen.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/pymatgen" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/pymatgen" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." monty-2024.7.29/docs/_config.yml000066400000000000000000000010211465166210500162660ustar00rootroot00000000000000title: monty email: ongsp@ucsd.edu description: >- # this means to ignore newlines until "baseurl:" A graph deep learning library for materials science. # twitter_username: jekyllrb github_username: materialsvirtuallab # Build settings remote_theme: just-the-docs/just-the-docs plugins: - jekyll-feed # favicon_ico: "/assets/favicon.ico" nav_external_links: - title: "monty on GitHub" url: "https://github.com/materialsvirtuallab/monty" aux_links: "Materials Virtual Lab": - "https://materialsvirtuallab.org"monty-2024.7.29/docs/_includes/000077500000000000000000000000001465166210500161125ustar00rootroot00000000000000monty-2024.7.29/docs/_includes/footer_custom.html000066400000000000000000000000731465166210500216700ustar00rootroot00000000000000© Copyright 2022, Materials Virtual Labmonty-2024.7.29/docs/_includes/nav_footer_custom.html000066400000000000000000000001151465166210500225310ustar00rootroot00000000000000© Copyright 2022, Materials Virtual Labmonty-2024.7.29/docs/changelog.md000066400000000000000000000226111465166210500164200ustar00rootroot00000000000000# Change log ## 2024.7.29 - Fix line ending in reverse_readfile/readline in Windows (@DanielYang59) - Add missing functools.wraps decorator to deprecated decorator and handle dataclass properly (@DanielYang59) - Add pint Quantity support to JSON (@rkingsbury) ## 2024.7.12 - Make cached_class decorated classes picklable (@janosh) - deprecated decorator allow replacement as string (@DanielYang59) - Fix (de)serialization datetime with timezone information (@DanielYang59) ## 2024.5.24 - Fix serious regression introduced in list_strings (@gmatteo) - Extend dev.deprecated to decorate classes and improve message (@DanielYang59) ## 2024.5.15 - Reimplemented support for pickle in MSONAble. (@matthewcarbone) ## 2024.4.17 - Revert changes to json.py for now. ## 2024.4.16 (yanked) - Misc bug fixes for jsanitize (@Andrew-S-Rosen). ## 2024.3.31 - Fix MSONable.REDIRECT when module name changed (@janosh) - Add native support for enums in jsanitize (@FabiPi3) - Make jsanitize(recursive_msonable=True) respect duck typing (@Andrew-S-Rosen) - Add optional arg target_dir in compress_file and decompress_file to allow specify target path (@DanielYang59) - Add MontyEncoder/MontyDecoder support for pathlib.Path (@Andrew-S-Rosen) - Add an optional arg deadline to dev.deprecated to raise warning after deadline (@DanielYang59) ## 2024.2.26 - Bug fix for symlinks when using copy_r (@Andrew-S-Rosen) ## 2024.2.2 - Bug fix for Enum subclasses with custom as_dict, from_dict (@jmmshn) ## 2024.1.26 - Fix import of optional libraries. ## 2024.1.23 - Lazy import of optional libraries to speed up startup. ## 2023.9.25 - Improved pydantic2 support (@munrojm). ## 2023.9.5 - Support pathlib.Path in shutil (@Andrew-S-Rosen). - Pydantic2 support (@munrojm). ## 2023.8.8 - Bug fix for decompress_file() to maintain backwards compatibility (@janosh) ## v2023.8.7 - Return path to decompressed file from decompress_file() (@janosh) - @deprecated change default category from FutureWarning to DeprecationWarning ## v2023.5.8 - Improved Pytorch tensor support for MontyEncoder/Decoder. - Bug fix to avoid torch dependency. ## v2023.5.7 - Pytorch tensor support for MontyEncoder/Decoder. ## v2023.4.10 -\* Fix for datetime support in jsanitize (@Andrew-S-Rosen). ## v2022.9.8 - Support for DataClasses in MontyEncoder, MontyDecoder and MSONable. ## v2022.4.26 -\* Fall back on json if orjson is not present. (@munrojm) ## v2022.3.12 -\* Allow recursive MSON in jsanitize (@Andrew-S-Rosen) - Option to use orjson for faster decoding. (@munrojm) ## v2022.1.19 -\* Fix ruamel.yaml backwards compatibility. ## v2022.1.12 -\* Fix decoding of dictionaries (@Andrew-S-Rosen). - Formal support for py3.10 ## v2021.12.1 -\* Adds support for lzma/xz format in zopen (@zhubonan). ## v2021.8.17 -\* Support serialization for Pandas DataFrames (@mkhorton). ## v2021.7.8 - Support the specification of `fmt` keyword arg in monty.serialization loadfn and dumpfn. ## v2021.6.10 -\* Expanded support for built-in functions, numpy types, etc. in MSON serialization (@utf). ## v2021.5.9 - Drop py3.5 support. ## v2021.3.3 - pydantic support (@shyamd) - UUID serialization support (@utf) ## v4.0.2 1. Allow specification of warning category in deprecated wrapper. ## v4.0.1 1. USe FutureWarning in the monty.dev.deprecated wrapper instead. ## v3.0.4 1. Add support for complex dtypes in MSONable numpy arrays. (@fracci) ## v3.0.3 1. Improvements to MSONAble to support Varidac Args (@shyamd). ## v3.0.1 1. Bug fixes for Windows. ## v3.0.0 1. Py3 only version. ## v2.0.7 1. MSONable now supports Enum types. (@mkhorton) ## v2.0.6 1. Revert py27 incompatible fmt spec for loadfn and dumpfn for now. This is a much less common use case. ## v2.0.5 1. Checking for extension type in loadfn and dumpfn now relies on ".json", ".yaml" or ".mpk". Further, now ".yml" and ".yaml" are both recognized as YAML files. (@clegaspi) 2. A fmt kwarg is now supported in loadfn and dumpfn to specify the format explicitly. (@clegaspi) ## v2.0.4 1. Bug fix for invert MSON caused by `@version`. ## v2.0.3 1. Support for nested MSONAble objects with MontyEncoder and dumpfn. (@davidwaroquiers) 2. Add @version to MSONAble. (@mkhorton) ## v2.0.0 1. Support for Path object in zopen. ## v1.0.5 1. Bug fix for io.reverse_readfile to ensure txt or binary string. ## v1.0.4 1. monty.shutil.remove which allows symlinks removal. Also improved monty.tempfile.ScratchDir cleanup. (@shyamd) ## v1.0.3 1. Bug fix for reverse_readfile for bz2 files (Alex Urban) ## v1.0.2 1. Misc bug fixes (tempdir on Windows) ## v1.0.1 1. Use CLoader and CDumper by default for speed. ## v1.0.0 1. Ruamel.yaml is now used as the default YAML parser and dumper. ## v0.9.8 1. Now ScratchDir functions as it should by replacing the original directory. ## v0.9.7 1. Minor update for inspect deprecation. ## v0.9.6 1. Allow private variable names (with leading underscores) to be auto-detected in default MSONable. ## v0.9.5 1. Favor use of inspect.signature in MSONAble. ## v0.9.3 1. Fix monty decoding of bson only if bson is present. ## v0.9.2 1. Minor update. ## v0.9.1 1. bson.objectid.ObjectId support for MontyEncoder and MontyDecoder. ## v0.9.0 1. Improved default as and from_dict. ## v0.8.5 1. Minor bug fixes. ## v0.8.4 1. Support for bson fields in jsanitize. ## v0.8.2 1. Fasetr gzip. ## v0.8.1 1. Update gcd for deprecated fractions.gcd in py >= 3.5. Try math.gcd by default first. ## v0.8.0 1. A new collections.tree object, which allows nested defaultdicts. ## v0.7.2 1. Added support for msgpack serialization in monty.serialization.dumpfn, loadfn and monty.msgpack.default and object_hook. ## v0.7.1 1. Added timeout function. Useful to limit function calls that take too long. ## v0.7.0 1. New backwards incompatible MSONable implementation that inspects init args to create a default dict representation for objects. ## v0.6.1 1. New jsanitize method to convert objects supporting the MSONable protocol to json serializable dicts. ## v0.6.0 1. New frozendict and MongoDict (allows for Javascript like access of nested dicts) classes (Matteo). 2. New Command class in subprocess which allows commands to be run in separate thread with timeout (Matteo). ## v0.5.9 1. More fixes for reverse read of gzipped files ofr Py3k. ## v0.5.8 1. Fix reverse read file for gzipped files. ## v0.5.7 1. Added a reverse_readfile method in monty.io, which is faster than reverse_readline for large files. ## v0.5.6 1. Provide way to specify Dumper and Loader in monty.serialization. 2. Better handling of unicode. ## v0.5.5 1. More robust handling of numpy arrays and datetime objects in json. 2. Refactor NotOverwritableDict to Namespace (Matteo). ## v0.5.4 1. Addition of many help functions in string, itertools, etc. (Matteo). 2. NullFile and NullStream in monty.design_patterns (Matteo). 3. FileLock in monty.io (Matteo) ## v0.5.3 1. Minor efficiency improvement. ## v0.5.2 1. Add unicode2str and str2unicode in monty.string. ## v0.5.0 1. Completely rewritten zopen which supports the "rt" keyword of Python 3 even when used in Python 2. 2. monty.string now has a marquee method which centers a string (contributed by Matteo). 3. Monty now supports only Python >= 3.3 as well as Python 2.7. Python 3.2 support is now dropped. ## v0.4.4 1. Refactor lazy_property to be in functools module. ## v0.4.3 1. Additional dev decorators lazy and logging functions. ## v0.4.2 1. Improve numpy array serialization with MontyEncoder. ## v0.4.1 1. Minor bug fix for module load in Py3k. ## v0.4.0 1. Remove deprecated json.loadf methods. 2. Add MSONable protocol for json/yaml based serialization. 3. deprecated now supports an additonal message. ## v0.3.6 1. :class:`monty.tempfile.ScratchDir` now checks for existence of root directory. If root path does not exist, will function as simple pass through. Makes it a lot more robust to bad mounting of scratch directories. ## v0.3.5 1. Added backport of functools.lru_cache. ## v0.3.4 1. Specialized json encoders / decoders with support for numpy arrays and objects supporting a to_dict() protocol used in pymatgen. ## v0.3.1 1. Proper support for libyaml auto-detect in yaml support. ## v0.3.0 1. Refactor serialization tools to shorten method names. ## v0.2.4 1. Added serialization module that supports both json and yaml. The latter requires pyyaml. ## v0.2.3 1. Added get_ncpus method in dev. (G. Matteo). ## v0.2.2 1. Add a Fabric-inspired cd context manager in monty.os. 2. Refactor ScratchDir context manager to monty.tempfile. ## v0.2.1 1. Add string module, which provides a function to remove non-ascii characters. More to be added. ## v0.2.0 1. ScratchDir now supports non-copying of files to and from current directory, and this is the default (different from prior releases). 2. Yet more improvements to copy_r to prevent recursive infinite loops in copying. ## v0.1.5 1. Added the useful monty.shutil.compress_file, compress_dir, decompress_file and decompress_dir methods. 2. Much more robust copy_r in shutil. ## v0.1.4 1. Bug fix for 0.1.3. ## v0.1.2 1. Added zpath method to return zipped paths. ## v0.1.1 1. Minor release to update description. ## v0.1.0 1. Ensure Python 3+ compatibility. 2. Travis testing implemented. ## v0.0.5 1. First official alpha release with unittests and docs. ## v0.0.2 1. Added several decorators and utilities. ## v0.0.1 1. Initial version. monty-2024.7.29/docs/conf.py000066400000000000000000000044371465166210500154540ustar00rootroot00000000000000# Configuration file for the Sphinx documentation builder. # # This file only contains a selection of the most common options. For a full # list see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html # -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # # import os # import sys # # sys.path.insert(0, os.path.abspath(".")) # sys.path.insert(0, os.path.dirname("..")) # sys.path.insert(0, os.path.dirname("../monty")) # sys.path.insert(0, os.path.dirname("../..")) # -- Project information ----------------------------------------------------- from __future__ import annotations project = "monty" copyright = "2022, Materials Virtual Lab" author = "Shyue Ping Ong" # -- General configuration --------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. # Napoleon is necessary to parse Google style docstrings. Markdown builder allows the generation of markdown output. extensions = [ "sphinx.ext.autodoc", "sphinx.ext.napoleon", "myst_parser", "sphinx_markdown_builder", ] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] autoclass_content = "both" # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. exclude_patterns = [] # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = "alabaster" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] myst_heading_anchors = 3 autodoc_default_options = {"private-members": False} monty-2024.7.29/docs/index.md000066400000000000000000000036451465166210500156060ustar00rootroot00000000000000--- layout: default title: Home nav_order: 1 --- [![GitHub license](https://img.shields.io/github/license/materialsvirtuallab/monty)](https://github.com/materialsvirtuallab/monty/blob/main/LICENSE) [![Linting](https://github.com/materialsvirtuallab/monty/workflows/Linting/badge.svg)](https://github.com/materialsvirtuallab/monty/workflows/Linting/badge.svg) [![Testing](https://github.com/materialsvirtuallab/monty/workflows/Testing/badge.svg)](https://github.com/materialsvirtuallab/monty/workflows/Testing/badge.svg) [![Downloads](https://static.pepy.tech/badge/monty)](https://pepy.tech/project/monty) [![codecov](https://codecov.io/gh/materialsvirtuallab/monty/branch/master/graph/badge.svg?token=QdfT2itxgu)](https://codecov.io/gh/materialsvirtuallab/monty) # Monty: Python Made Even Easier Monty is the missing complement to Python. Monty implements supplementary useful functions for Python that are not part of the standard library. Examples include useful utilities like transparent support for zipped files, useful design patterns such as singleton and cached_class, and many more. Python is a great programming language and comes with "batteries included". However, even Python has missing functionality and/or quirks that make it more difficult to do many simple tasks. In the process of creating several large scientific frameworks based on Python, my co-developers and I have found that it is often useful to create reusable utility functions to supplement the Python standard library. Our forays in various developer sites and forums also found that many developers are looking for solutions to the same problems. Monty is created to serve as a complement to the Python standard library. It provides suite of tools to solve many common problems, and hopefully, be a resource to collect the best solutions. Monty supports Python 3.x. Please visit the [official docs](https://materialsvirtuallab.github.io/monty) for more information. monty-2024.7.29/docs/make.bat000066400000000000000000000117541465166210500155620ustar00rootroot00000000000000@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\pymatgen.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\pymatgen.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) :end monty-2024.7.29/docs/monty.bisect.md000066400000000000000000000014631465166210500171110ustar00rootroot00000000000000--- layout: default title: monty.bisect.md nav_exclude: true --- # monty.bisect module Additional bisect functions. Taken from [https://docs.python.org/2/library/bisect.html](https://docs.python.org/2/library/bisect.html) The above bisect() functions are useful for finding insertion points but can be tricky or awkward to use for common searching tasks. The functions show how to transform them into the standard lookups for sorted lists. ## monty.bisect.find_ge(a, x) Find leftmost item greater than or equal to x. ## monty.bisect.find_gt(a, x) Find leftmost value greater than x. ## monty.bisect.find_le(a, x) Find rightmost value less than or equal to x. ## monty.bisect.find_lt(a, x) Find rightmost value less than x. ## monty.bisect.index(a, x, atol=None) Locate the leftmost value exactly equal to x.monty-2024.7.29/docs/monty.collections.md000066400000000000000000000073241465166210500201600ustar00rootroot00000000000000--- layout: default title: monty.collections.md nav_exclude: true --- # monty.collections module Useful collection classes, e.g., tree, frozendict, etc. ## *class* monty.collections.AttrDict(\*args, \*\*kwargs) Bases: `dict` Allows to access dict keys as obj.foo in addition to the traditional way obj[‘foo’]” ## Example ```python >>> d = AttrDict(foo=1, bar=2) >>> assert d["foo"] == d.foo >>> d.bar = "hello" >>> assert d.bar == "hello" ``` * **Parameters** * **args** – Passthrough arguments for standard dict. * **kwargs** – Passthrough keyword arguments for standard dict. ### copy() * **Returns** Copy of AttrDict ## *class* monty.collections.FrozenAttrDict(\*args, \*\*kwargs) Bases: `frozendict` A dictionary that: ```none * does not permit changes. * Allows to access dict keys as obj.foo in addition to the traditional way obj[‘foo’] ``` * **Parameters** * **args** – Passthrough arguments for standard dict. * **kwargs** – Passthrough keyword arguments for standard dict. ## *class* monty.collections.MongoDict(\*args, \*\*kwargs) Bases: `object` This dict-like object allows one to access the entries in a nested dict as attributes. Entries (attributes) cannot be modified. It also provides Ipython tab completion hence this object is particularly useful if you need to analyze a nested dict interactively (e.g. documents extracted from a MongoDB database). ```python >>> m = MongoDict({'a': {'b': 1}, 'x': 2}) >>> assert m.a.b == 1 and m.x == 2 >>> assert "a" in m and "b" in m.a >>> m["a"] {'b': 1} ``` **NOTE**: Cannot inherit from ABC collections.Mapping because otherwise dict.keys and dict.items will pollute the namespace. e.g MongoDict({“keys”: 1}).keys would be the ABC dict method. * **Parameters** * **args** – Passthrough arguments for standard dict. * **kwargs** – Passthrough keyword arguments for standard dict. ## *class* monty.collections.Namespace(\*args, \*\*kwargs) Bases: `dict` A dictionary that does not permit to redefine its keys. * **Parameters** * **args** – Passthrough arguments for standard dict. * **kwargs** – Passthrough keyword arguments for standard dict. ### update(\*args, \*\*kwargs) * **Parameters** * **args** – Passthrough arguments for standard dict. * **kwargs** – Passthrough keyword arguments for standard dict. ## monty.collections.dict2namedtuple(\*args, \*\*kwargs) Helper function to create a `namedtuple` from a dictionary. ## Example ```python >>> t = dict2namedtuple(foo=1, bar="hello") >>> assert t.foo == 1 and t.bar == "hello" ``` ```python >>> t = dict2namedtuple([("foo", 1), ("bar", "hello")]) >>> assert t[0] == t.foo and t[1] == t.bar ``` **WARNING**: * The order of the items in the namedtuple is not deterministic if kwargs are used. namedtuples, however, should always be accessed by attribute hence this behaviour should not represent a serious problem. * Don’t use this function in code in which memory and performance are crucial since a dict is needed to instantiate the tuple! ## *class* monty.collections.frozendict(\*args, \*\*kwargs) Bases: `dict` A dictionary that does not permit changes. The naming violates PEP8 to be consistent with standard Python’s “frozenset” naming. * **Parameters** * **args** – Passthrough arguments for standard dict. * **kwargs** – Passthrough keyword arguments for standard dict. ### update(\*args, \*\*kwargs) * **Parameters** * **args** – Passthrough arguments for standard dict. * **kwargs** – Passthrough keyword arguments for standard dict. ## monty.collections.tree() A tree object, which is effectively a recursive defaultdict that adds tree as members. Usage: ```none x = tree() x[‘a’][‘b’][‘c’] = 1 ``` * **Returns** A tree.monty-2024.7.29/docs/monty.design_patterns.md000066400000000000000000000025031465166210500210250ustar00rootroot00000000000000--- layout: default title: monty.design_patterns.md nav_exclude: true --- # monty.design_patterns module Some common design patterns such as singleton and cached classes. ## *class* monty.design_patterns.NullFile() Bases: `object` A file object that is associated to /dev/null. no-op ## *class* monty.design_patterns.NullStream() Bases: `object` A fake stream with a no-op write. ### write(\*args) Does nothing… :param args: ## monty.design_patterns.cached_class(klass: type[Klass]) Decorator to cache class instances by constructor arguments. This results in a class that behaves like a singleton for each set of constructor arguments, ensuring efficiency. Note that this should be used for *immutable classes only*. Having a cached mutable class makes very little sense. For efficiency, avoid using this decorator for situations where there are many constructor arguments permutations. The keywords argument dictionary is converted to a tuple because dicts are mutable; keywords themselves are strings and so are always hashable, but if any arguments (keyword or positional) are non-hashable, that set of arguments is not cached. ## monty.design_patterns.singleton(cls) This decorator can be used to create a singleton out of a class. Usage: ```default @singleton class MySingleton(): def __init__(): pass ```monty-2024.7.29/docs/monty.dev.md000066400000000000000000000044221465166210500164140ustar00rootroot00000000000000--- layout: default title: monty.dev.md nav_exclude: true --- # monty.dev module This module implements several useful functions and decorators that can be particularly useful for developers. E.g., deprecating methods / classes, etc. ## monty.dev.deprecated(replacement=None, message=None, category=) Decorator to mark classes or functions as deprecated, with a possible replacement. * **Parameters** * **replacement** (*callable*) – A replacement class or method. * **message** (*str*) – A warning message to be displayed. * **category** (*Warning*) – Choose the category of the warning to issue. Defaults to FutureWarning. Another choice can be DeprecationWarning. Note that FutureWarning is meant for end users and is always shown unless silenced. DeprecationWarning is meant for developers and is never shown unless python is run in developmental mode or the filter is changed. Make the choice accordingly. * **Returns** Original function, but with a warning to use the updated class. ## monty.dev.install_excepthook(hook_type=’color’, \*\*kwargs) This function replaces the original python traceback with an improved version from Ipython. Use color for colourful traceback formatting, verbose for Ka-Ping Yee’s “cgitb.py” version kwargs are the keyword arguments passed to the constructor. See IPython.core.ultratb.py for more info. * **Returns** 0 if hook is installed successfully. ## *class* monty.dev.requires(condition, message) Bases: `object` Decorator to mark classes or functions as requiring a specified condition to be true. This can be used to present useful error messages for optional dependencies. For example, decorating the following code will check if scipy is present and if not, a runtime error will be raised if someone attempts to call the use_scipy function: ```default try: import scipy except ImportError: scipy = None @requires(scipy is not None, "scipy is not present.") def use_scipy(): print(scipy.majver) ``` * **Parameters** * **condition** – Condition necessary to use the class or function. * **message** – A message to be displayed if the condition is not True. * **condition** – A expression returning a bool. * **message** – Message to display if condition is False.monty-2024.7.29/docs/monty.fnmatch.md000066400000000000000000000014751465166210500172630ustar00rootroot00000000000000--- layout: default title: monty.fnmatch.md nav_exclude: true --- # monty.fnmatch module This module provides support for Unix shell-style wildcards ## *class* monty.fnmatch.WildCard(wildcard, sep=’|’) Bases: `object` This object provides an easy-to-use interface for filename matching with shell patterns (fnmatch). ```python >>> w = WildCard("*.nc|*.pdf") >>> w.filter(["foo.nc", "bar.pdf", "hello.txt"]) ['foo.nc', 'bar.pdf'] ``` ```python >>> w.filter("foo.nc") ['foo.nc'] ``` Initializes a WildCard. * **Parameters** * **wildcard** (*str*) – String of tokens separated by sep. Each token represents a pattern. * **sep** (*str*) – Separator for shell patterns. ### filter(names) Returns a list with the names matching the pattern. ### match(name) Returns True if name matches one of the patterns.monty-2024.7.29/docs/monty.fractions.md000066400000000000000000000015261465166210500176300ustar00rootroot00000000000000--- layout: default title: monty.fractions.md nav_exclude: true --- # monty.fractions module Math functions. ## monty.fractions.gcd(\*numbers) Returns the greatest common divisor for a sequence of numbers. * **Parameters** **\*numbers** – Sequence of numbers. * **Returns** (int) Greatest common divisor of numbers. ## monty.fractions.gcd_float(numbers, tol=1e-08) Returns the greatest common divisor for a sequence of numbers. Uses a numerical tolerance, so can be used on floats * **Parameters** * **numbers** – Sequence of numbers. * **tol** – Numerical tolerance * **Returns** (int) Greatest common divisor of numbers. ## monty.fractions.lcm(\*numbers) Return lowest common multiple of a sequence of numbers. * **Parameters** **\*numbers** – Sequence of numbers. * **Returns** (int) Lowest common multiple of numbers.monty-2024.7.29/docs/monty.functools.md000066400000000000000000000071401465166210500176520ustar00rootroot00000000000000--- layout: default title: monty.functools.md nav_exclude: true --- # monty.functools module functools, especially backported from Python 3. ## *exception* monty.functools.TimeoutError(message) Bases: `Exception` Exception class for timeouts. * **Parameters** **message** – Error message ## *class* monty.functools.lazy_property(func) Bases: `object` lazy_property descriptor Used as a decorator to create lazy attributes. Lazy attributes are evaluated on first use. * **Parameters** **func** – Function to decorate. ### *classmethod* invalidate(inst, name) Invalidate a lazy attribute. This obviously violates the lazy contract. A subclass of lazy may however have a contract where invalidation is appropriate. ## monty.functools.lru_cache(maxsize=128, typed=False) Least-recently-used cache decorator, which is a backport of the same function in Python >= 3.2. If *maxsize* is set to None, the LRU features are disabled and the cache can grow without bound. If *typed* is True, arguments of different types will be cached separately. For example, f(3.0) and f(3) will be treated as distinct calls with distinct results. Arguments to the cached function must be hashable. View the cache statistics named tuple (hits, misses, maxsize, currsize) with f.cache_info(). Clear the cache and statistics with f.cache_clear(). Access the underlying function with f.**wrapped**. See: [http://en.wikipedia.org/wiki/Cache_algorithms#Least_Recently_Used](http://en.wikipedia.org/wiki/Cache_algorithms#Least_Recently_Used) ## monty.functools.prof_main(main) Decorator for profiling main programs. Profiling is activated by prepending the command line options supported by the original main program with the keyword prof. .. rubric:: Example $ script.py arg –foo=1 becomes > $ script.py prof arg –foo=1 The decorated main accepts two new arguments: > prof_file: Name of the output file with profiling data > ```none > If not given, a temporary file is created. > ``` > sortby: Profiling data are sorted according to this value. > ```none > default is “time”. See sort_stats. > ``` ## monty.functools.return_if_raise(exception_tuple, retval_if_exc, disabled=False) Decorator for functions, methods or properties. Execute the callable in a try block, and return retval_if_exc if one of the exceptions listed in exception_tuple is raised (se also `return_node_if_raise`). Setting disabled to True disables the try except block (useful for debugging purposes). One can use this decorator to define properties. Example: ```default @return_if_raise(ValueError, None) def return_none_if_value_error(self): pass @return_if_raise((ValueError, KeyError), "hello") def another_method(self): pass @property @return_if_raise(AttributeError, None) def name(self): "Name of the object, None if not set." return self._name ``` ## monty.functools.return_none_if_raise(exception_tuple, \*, retval_if_exc=None, disabled=False) This decorator returns None if one of the exceptions is raised. > @return_none_if_raise(ValueError) > def method(self): ## *class* monty.functools.timeout(seconds=1, error_message=’Timeout’) Bases: `object` Timeout function. Use to limit matching to a certain time limit. Note that this works only on Unix-based systems as it uses signal. Usage: try: ```none with timeout(3): do_stuff() ``` except TimeoutError: ```none do_something_else() ``` * **Parameters** * **seconds** (*int*) – Allowed time for function in seconds. * **error_message** (*str*) – An error message. ### handle_timeout(signum, frame) * **Parameters** * **signum** – Return signal from call. * **frame** –monty-2024.7.29/docs/monty.inspect.md000066400000000000000000000015641465166210500173070ustar00rootroot00000000000000--- layout: default title: monty.inspect.md nav_exclude: true --- # monty.inspect module Useful additional functions to help get information about live objects ## monty.inspect.all_subclasses(cls) Given a class cls, this recursive function returns a list with all subclasses, subclasses of subclasses, and so on. ## monty.inspect.caller_name(skip=2) Get a name of a caller in the format module.class.method skip specifies how many levels of stack to skip while getting caller name. skip=1 means “who calls me”, skip=2 “who calls my caller” etc. An empty string is returned if skipped levels exceed stack height Taken from: > [https://gist.github.com/techtonik/2151727](https://gist.github.com/techtonik/2151727) Public Domain, i.e. feel free to copy/paste ## monty.inspect.find_top_pyfile() This function inspects the Cpython frame to find the path of the script.monty-2024.7.29/docs/monty.io.md000066400000000000000000000072721465166210500162530ustar00rootroot00000000000000--- layout: default title: monty.io.md nav_exclude: true --- # monty.io module Augments Python’s suite of IO functions with useful transparent support for compressed files. ## *class* monty.io.FileLock(file_name, timeout=10, delay=0.05) Bases: `object` A file locking mechanism that has context-manager support so you can use it in a with statement. This should be relatively cross-compatible as it doesn’t rely on msvcrt or fcntl for the locking. Taken from [http://www.evanfosmark.com/2009/01/cross-platform-file-locking](http://www.evanfosmark.com/2009/01/cross-platform-file-locking) -support-in-python/ Prepare the file locker. Specify the file to lock and optionally the maximum timeout and the delay between each attempt to lock. * **Parameters** * **file_name** – Name of file to lock. * **timeout** – Maximum timeout for locking. Defaults to 10. * **delay** – Delay between each attempt to lock. Defaults to 0.05. ### Error() alias of `FileLockException` ### acquire() Acquire the lock, if possible. If the lock is in use, it check again every delay seconds. It does this until it either gets the lock or exceeds timeout number of seconds, in which case it throws an exception. ### release() Get rid of the lock by deleting the lockfile. When working in a with statement, this gets automatically called at the end. ## *exception* monty.io.FileLockException() Bases: `Exception` Exception raised by FileLock. ## monty.io.get_open_fds() Return the number of open file descriptors for current process ## monty.io.reverse_readfile(filename: str | Path) A much faster reverse read of file by using Python’s mmap to generate a memory-mapped file. It is slower for very small files than reverse_readline, but at least 2x faster for large files (the primary use of such a method). * **Parameters** **filename** (*str*) – Name of file to read. * **Yields** Lines from the file in reverse order. ## monty.io.reverse_readline(m_file, blk_size=4096, max_mem=4000000) Generator method to read a file line-by-line, but backwards. This allows one to efficiently get data at the end of a file. Based on code by Peter Astrand <[astrand@cendio.se](mailto:astrand@cendio.se)>, using modifications by Raymond Hettinger and Kevin German. [http://code.activestate.com/recipes/439045-read-a-text-file-backwards](http://code.activestate.com/recipes/439045-read-a-text-file-backwards) -yet-another-implementat/ Reads file forwards and reverses in memory for files smaller than the max_mem parameter, or for gzip files where reverse seeks are not supported. Files larger than max_mem are dynamically read backwards. * **Parameters** * **m_file** (*File*) – File stream to read (backwards) * **blk_size** (*int*) – The buffer size. Defaults to 4096. * **max_mem** (*int*) – The maximum amount of memory to involve in this operation. This is used to determine when to reverse a file in-memory versus seeking portions of a file. For bz2 files, this sets the maximum block size. * **Returns** Generator that returns lines from the file. Similar behavior to the file.readline() method, except the lines are returned from the back of the file. ## monty.io.zopen(filename: str | Path, \*args, \*\*kwargs) This function wraps around the bz2, gzip, lzma, xz and standard python’s open function to deal intelligently with bzipped, gzipped or standard text files. * **Parameters** * **filename** (*str/Path*) – filename or pathlib.Path. * **\*args** – Standard args for python open(..). E.g., ‘r’ for read, ‘w’ for write. * **\*\*kwargs** – Standard kwargs for python open(..). * **Returns** File-like object. Supports with context.monty-2024.7.29/docs/monty.itertools.md000066400000000000000000000031621465166210500176620ustar00rootroot00000000000000--- layout: default title: monty.itertools.md nav_exclude: true --- # monty.itertools module Additional tools for iteration. ## monty.itertools.chunks(items, n) Yield successive n-sized chunks from a list-like object. ```python >>> import pprint >>> pprint.pprint(list(chunks(range(1, 25), 10))) [(1, 2, 3, 4, 5, 6, 7, 8, 9, 10), (11, 12, 13, 14, 15, 16, 17, 18, 19, 20), (21, 22, 23, 24)] ``` ## monty.itertools.ilotri(items, diago=True, with_inds=False) A generator that yields the lower triangle of the matrix (items x items) * **Parameters** * **items** – Iterable object with elements [e0, e1, …] * **diago** – False if diagonal matrix elements should be excluded * **with_inds** – If True, (i,j) (e_i, e_j) is returned else (e_i, e_j) ```python >>> for (ij, mate) in ilotri([0,1], with_inds=True): ... print("ij:", ij, "mate:", mate) ij: (0, 0) mate: (0, 0) ij: (1, 0) mate: (1, 0) ij: (1, 1) mate: (1, 1) ``` ## monty.itertools.iterator_from_slice(s) Constructs an iterator given a slice object s. **NOTE**: The function returns an infinite iterator if s.stop is None ## monty.itertools.iuptri(items, diago=True, with_inds=False) A generator that yields the upper triangle of the matrix (items x items) * **Parameters** * **items** – Iterable object with elements [e0, e1, …] * **diago** – False if diagonal matrix elements should be excluded * **with_inds** – If True, (i,j) (e_i, e_j) is returned else (e_i, e_j) ```python >>> for (ij, mate) in iuptri([0,1], with_inds=True): ... print("ij:", ij, "mate:", mate) ij: (0, 0) mate: (0, 0) ij: (0, 1) mate: (0, 1) ij: (1, 1) mate: (1, 1) ```monty-2024.7.29/docs/monty.json.md000066400000000000000000000210171465166210500166060ustar00rootroot00000000000000--- layout: default title: monty.json.md nav_exclude: true --- # monty.json module JSON serialization and deserialization utilities. ## *exception* monty.json.MSONError() Bases: `Exception` Exception class for serialization errors. ## *class* monty.json.MSONable() Bases: `object` This is a mix-in base class specifying an API for msonable objects. MSON is Monty JSON. Essentially, MSONable objects must implement an as_dict method, which must return a json serializable dict and must also support no arguments (though optional arguments to finetune the output is ok), and a from_dict class method that regenerates the object from the dict generated by the as_dict method. The as_dict method should contain the “@module” and “@class” keys which will allow the MontyEncoder to dynamically deserialize the class. E.g.: ```default d["@module"] = self.__class__.__module__ d["@class"] = self.__class__.__name__ ``` A default implementation is provided in MSONable, which automatically determines if the class already contains self.argname or self._argname attributes for every arg. If so, these will be used for serialization in the dict format. Similarly, the default from_dict will deserialization classes of such form. An example is given below: ```default class MSONClass(MSONable): def __init__(self, a, b, c, d=1, **kwargs): self.a = a self.b = b self._c = c self._d = d self.kwargs = kwargs ``` For such classes, you merely need to inherit from MSONable and you do not need to implement your own as_dict or from_dict protocol. New to Monty V2.0.6…. Classes can be redirected to moved implementations by putting in the old fully qualified path and new fully qualified path into .monty.yaml in the home folder Example: old_module.old_class: new_module.new_class ### REDIRECT(_ = {_ ) ### as_dict() A JSON serializable dict representation of an object. ### *classmethod* from_dict(d) * **Parameters** **d** – Dict representation. * **Returns** MSONable class. ### to_json() Returns a json string representation of the MSONable object. ### unsafe_hash() Returns an hash of the current object. This uses a generic but low performance method of converting the object to a dictionary, flattening any nested keys, and then performing a hash on the resulting object ### *classmethod* validate_monty(v) pydantic Validator for MSONable pattern ## *class* monty.json.MontyDecoder(\*, object_hook=None, parse_float=None, parse_int=None, parse_constant=None, strict=True, object_pairs_hook=None) Bases: `JSONDecoder` A Json Decoder which supports the MSONable API. By default, the decoder attempts to find a module and name associated with a dict. If found, the decoder will generate a Pymatgen as a priority. If that fails, the original decoded dictionary from the string is returned. Note that nested lists and dicts containing pymatgen object will be decoded correctly as well. Usage: `object_hook`, if specified, will be called with the result of every JSON object decoded and its return value will be used in place of the given `dict`. This can be used to provide custom deserializations (e.g. to support JSON-RPC class hinting). `object_pairs_hook`, if specified will be called with the result of every JSON object decoded with an ordered list of pairs. The return value of `object_pairs_hook` will be used instead of the `dict`. This feature can be used to implement custom decoders. If `object_hook` is also defined, the `object_pairs_hook` takes priority. `parse_float`, if specified, will be called with the string of every JSON float to be decoded. By default this is equivalent to float(num_str). This can be used to use another datatype or parser for JSON floats (e.g. decimal.Decimal). `parse_int`, if specified, will be called with the string of every JSON int to be decoded. By default this is equivalent to int(num_str). This can be used to use another datatype or parser for JSON integers (e.g. float). `parse_constant`, if specified, will be called with one of the following strings: -Infinity, Infinity, NaN. This can be used to raise an exception if invalid JSON numbers are encountered. If `strict` is false (true is the default), then control characters will be allowed inside strings. Control characters in this context are those with character codes in the 0-31 range, including `'\\\\\\\\t'` (tab), `'\\\\\\\\n'`, `'\\\\\\\\r'` and `'\\\\\\\\0'`. # Add it as a *cls* keyword when using json.load json.loads(json_string, cls=MontyDecoder) ## decode(s) Overrides decode from JSONDecoder. * **Parameters** **s** – string * **Returns** Object. ## process_decoded(d) Recursive method to support decoding dicts and lists containing pymatgen objects. ## *class* monty.json.MontyEncoder(\*, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, sort_keys=False, indent=None, separators=None, default=None) Bases: `JSONEncoder` A Json Encoder which supports the MSONable API, plus adds support for numpy arrays, datetime objects, bson ObjectIds (requires bson). Usage: ```default # Add it as a *cls* keyword when using json.dump json.dumps(object, cls=MontyEncoder) ``` Constructor for JSONEncoder, with sensible defaults. If skipkeys is false, then it is a TypeError to attempt encoding of keys that are not str, int, float or None. If skipkeys is True, such items are simply skipped. If ensure_ascii is true, the output is guaranteed to be str objects with all incoming non-ASCII characters escaped. If ensure_ascii is false, the output can contain non-ASCII characters. If check_circular is true, then lists, dicts, and custom encoded objects will be checked for circular references during encoding to prevent an infinite recursion (which would cause an RecursionError). Otherwise, no such check takes place. If allow_nan is true, then NaN, Infinity, and -Infinity will be encoded as such. This behavior is not JSON specification compliant, but is consistent with most JavaScript based encoders and decoders. Otherwise, it will be a ValueError to encode such floats. If sort_keys is true, then the output of dictionaries will be sorted by key; this is useful for regression tests to ensure that JSON serializations can be compared on a day-to-day basis. If indent is a non-negative integer, then JSON array elements and object members will be pretty-printed with that indent level. An indent level of 0 will only insert newlines. None is the most compact representation. If specified, separators should be an (item_separator, key_separator) tuple. The default is (’, ‘, ‘: ‘) if *indent* is `None` and (‘,’, ‘: ‘) otherwise. To get the most compact JSON representation, you should specify (‘,’, ‘:’) to eliminate whitespace. If specified, default is a function that gets called for objects that can’t otherwise be serialized. It should return a JSON encodable version of the object or raise a `TypeError`. ### default(o) Overriding default method for JSON encoding. This method does two things: (a) If an object has a to_dict property, return the to_dict output. (b) If the @module and @class keys are not in the to_dict, add them to the output automatically. If the object has no to_dict property, the default Python json encoder default method is called. :param o: Python object. * **Returns** Python dict representation. ## monty.json.jsanitize(obj, strict=False, allow_bson=False, enum_values=False, recursive_msonable=False) This method cleans an input json-like object, either a list or a dict or some sequence, nested or otherwise, by converting all non-string dictionary keys (such as int and float) to strings, and also recursively encodes all objects using Monty’s as_dict() protocol. * **Parameters** * **obj** – input json-like object. * **strict** (*bool*) – This parameters sets the behavior when jsanitize encounters an object it does not understand. If strict is True, jsanitize will try to get the as_dict() attribute of the object. If no such attribute is found, an attribute error will be thrown. If strict is False, jsanitize will simply call str(object) to convert the object to a string representation. * **allow_bson** (*bool*) – This parameters sets the behavior when jsanitize encounters a bson supported type such as objectid and datetime. If True, such bson types will be ignored, allowing for proper insertion into MongoDB databases. * **enum_values** (*bool*) – Convert Enums to their values. * **recursive_msonable** (*bool*) – If True, uses .as_dict() for MSONables regardless of the value of strict. * **Returns** Sanitized dict that can be json serialized.monty-2024.7.29/docs/monty.logging.md000066400000000000000000000012631465166210500172640ustar00rootroot00000000000000--- layout: default title: monty.logging.md nav_exclude: true --- # monty.logging module Logging tools ## monty.logging.enable_logging(main) This decorator is used to decorate main functions. It adds the initialization of the logger and an argument parser that allows one to select the loglevel. Useful if we are writing simple main functions that call libraries where the logging module is used * **Parameters** **main** – main function. ## monty.logging.logged(level=10) Useful logging decorator. If a method is logged, the beginning and end of the method call will be logged at a pre-specified level. * **Parameters** **level** – Level to log method at. Defaults to DEBUG.monty-2024.7.29/docs/monty.math.md000066400000000000000000000006661465166210500165750ustar00rootroot00000000000000--- layout: default title: monty.math.md nav_exclude: true --- # monty.math module Addition math functions. ## monty.math.nCr(n, r) Calculates nCr. * **Parameters** * **n** (*int*) – total number of items. * **r** (*int*) – items to choose * **Returns** nCr. ## monty.math.nPr(n, r) Calculates nPr. * **Parameters** * **n** (*int*) – total number of items. * **r** (*int*) – items to permute * **Returns** nPr.monty-2024.7.29/docs/monty.md000066400000000000000000000074131465166210500156420ustar00rootroot00000000000000--- layout: default title: API Documentation nav_order: 5 --- # monty package Monty is the missing complement to Python. Monty implements supplementary useful functions for Python that are not part of the standard library. Examples include useful utilities like transparent support for zipped files, useful design patterns such as singleton and cached_class, and many more. ## Subpackages * [monty.os package](monty.os.md) * `cd()` * `makedirs_p()` * [monty.os.path module](monty.os.path.md) * `find_exts()` * `zpath()` * [monty.bisect module](monty.bisect.md) * `find_ge()` * `find_gt()` * `find_le()` * `find_lt()` * `index()` * [monty.collections module](monty.collections.md) * `AttrDict` * `AttrDict.copy()` * `FrozenAttrDict` * `MongoDict` * `Namespace` * `Namespace.update()` * `dict2namedtuple()` * `frozendict` * `frozendict.update()` * `tree()` * [monty.design_patterns module](monty.design_patterns.md) * `NullFile` * `NullStream` * `NullStream.write()` * `cached_class()` * `singleton()` * [monty.dev module](monty.dev.md) * `deprecated()` * `install_excepthook()` * `requires` * [monty.fnmatch module](monty.fnmatch.md) * `WildCard` * `WildCard.filter()` * `WildCard.match()` * [monty.fractions module](monty.fractions.md) * `gcd()` * `gcd_float()` * `lcm()` * [monty.functools module](monty.functools.md) * `TimeoutError` * `lazy_property` * `lazy_property.invalidate()` * `lru_cache()` * `prof_main()` * `return_if_raise()` * `return_none_if_raise()` * `timeout` * `timeout.handle_timeout()` * [monty.inspect module](monty.inspect.md) * `all_subclasses()` * `caller_name()` * `find_top_pyfile()` * [monty.io module](monty.io.md) * `FileLock` * `FileLock.Error` * `FileLock.acquire()` * `FileLock.release()` * `FileLockException` * `get_open_fds()` * `reverse_readfile()` * `reverse_readline()` * `zopen()` * [monty.itertools module](monty.itertools.md) * `chunks()` * `ilotri()` * `iterator_from_slice()` * `iuptri()` * [monty.json module](monty.json.md) * `MSONError` * `MSONable` * `MSONable.REDIRECT` * `MSONable.as_dict()` * `MSONable.from_dict()` * `MSONable.to_json()` * `MSONable.unsafe_hash()` * `MSONable.validate_monty()` * `MontyDecoder` * `MontyDecoder.decode()` * `MontyDecoder.process_decoded()` * `MontyEncoder` * `MontyEncoder.default()` * `jsanitize()` * [monty.logging module](monty.logging.md) * `enable_logging()` * `logged()` * [monty.math module](monty.math.md) * `nCr()` * `nPr()` * [monty.msgpack module](monty.msgpack.md) * `default()` * `object_hook()` * [monty.multiprocessing module](monty.multiprocessing.md) * `imap_tqdm()` * [monty.operator module](monty.operator.md) * `operator_from_str()` * [monty.pprint module](monty.pprint.md) * `DisplayEcoder` * `DisplayEcoder.default()` * `draw_tree()` * `pprint_json()` * `pprint_table()` * [monty.re module](monty.re.md) * `regrep()` * [monty.serialization module](monty.serialization.md) * `dumpfn()` * `loadfn()` * [monty.shutil module](monty.shutil.md) * `compress_dir()` * `compress_file()` * `copy_r()` * `decompress_dir()` * `decompress_file()` * `gzip_dir()` * `remove()` * [monty.string module](monty.string.md) * `boxed()` * `indent()` * `is_string()` * `list_strings()` * `make_banner()` * `marquee()` * `remove_non_ascii()` * `unicode2str()` * [monty.subprocess module](monty.subprocess.md) * `Command` * `Command.retcode` * `Command.killed` * `Command.output` * `Command.error` * `Command.run()` * [monty.tempfile module](monty.tempfile.md) * `ScratchDir` * `ScratchDir.SCR_LINK` * [monty.termcolor module](monty.termcolor.md) * `colored()` * `cprint()`monty-2024.7.29/docs/monty.msgpack.md000066400000000000000000000011311465166210500172550ustar00rootroot00000000000000--- layout: default title: monty.msgpack.md nav_exclude: true --- # monty.msgpack module msgpack serialization and deserialization utilities. Right now, this is a stub using monty.json encoder and decoders. The naming is just for clearer usage with msgpack’s default and object_hook naming. ## monty.msgpack.default(obj) For use with msgpack.packb(obj, default=default). Supports Monty’s as_dict protocol, numpy arrays and datetime. ## monty.msgpack.object_hook(d) For use with msgpack.unpackb(dict, object_hook=object_hook.). Supports Monty’s as_dict protocol, numpy arrays and datetime.monty-2024.7.29/docs/monty.multiprocessing.md000066400000000000000000000011371465166210500210650ustar00rootroot00000000000000--- layout: default title: monty.multiprocessing.md nav_exclude: true --- # monty.multiprocessing module Multiprocessing utilities. ## monty.multiprocessing.imap_tqdm(nprocs: int, func: Callable, iterable: Iterable, \*args, \*\*kwargs) A wrapper around Pool.imap. Creates a Pool with nprocs and then runs a f unction over an iterable with progress bar. * **Parameters** * **nprocs** – Number of processes * **func** – Callable * **iterable** – Iterable of arguments. * **args** – Passthrough to Pool.imap * **kwargs** – Passthrough to Pool.imap * **Returns** Results of Pool.imap.monty-2024.7.29/docs/monty.operator.md000066400000000000000000000004101465166210500174620ustar00rootroot00000000000000--- layout: default title: monty.operator.md nav_exclude: true --- # monty.operator module Useful additional functions for operators ## monty.operator.operator_from_str(op) Return the operator associated to the given string op. * **Raises** **KeyError** –monty-2024.7.29/docs/monty.os.md000066400000000000000000000014611465166210500162570ustar00rootroot00000000000000--- layout: default title: monty.os.md nav_exclude: true --- # monty.os package Os functions, e.g., cd, makedirs_p. ## monty.os.cd(path) A Fabric-inspired cd context that temporarily changes directory for performing some tasks, and returns to the original working directory afterwards. E.g., > with cd(“/my/path/”): > ```none > do_something() > ``` * **Parameters** **path** – Path to cd to. ## monty.os.makedirs_p(path, \*\*kwargs) Wrapper for os.makedirs that does not raise an exception if the directory already exists, in the fashion of “mkdir -p” command. The check is performed in a thread-safe way * **Parameters** * **path** – path of the directory to create * **kwargs** – standard kwargs for os.makedirs * [monty.os.path module](monty.os.path.md) * `find_exts()` * `zpath()`monty-2024.7.29/docs/monty.os.path.md000066400000000000000000000033361465166210500172150ustar00rootroot00000000000000--- layout: default title: monty.os.path.md nav_exclude: true --- # monty.os.path module Path based methods, e.g., which, zpath, etc. ## monty.os.path.find_exts(top, exts, exclude_dirs=None, include_dirs=None, match_mode=’basename’) Find all files with the extension listed in exts that are located within the directory tree rooted at top (including top itself, but excluding ‘.’ and ‘..’) * **Parameters** * **top** (*str*) – Root directory * **exts** (*str*\* or **list** of \**strings*) – List of extensions. * **exclude_dirs** (*str*) – Wildcards used to exclude particular directories. Can be concatenated via | * **include_dirs** (*str*) – Wildcards used to select particular directories. include_dirs and exclude_dirs are mutually exclusive * **match_mode** (*str*) – “basename” if match should be done on the basename. “abspath” for absolute path. * **Returns** Absolute paths of the files. * **Return type** (list of str) Examples: ```default # Find all pdf and ps files starting from the current directory. find_exts(".", ("pdf", "ps")) # Find all pdf files, exclude hidden directories and dirs whose name # starts with `_` find_exts(".", "pdf", exclude_dirs="_*|.*") # Find all ps files, in the directories whose basename starts with # output. find_exts(".", "ps", include_dirs="output*")) ``` ## monty.os.path.zpath(filename) Returns an existing (zipped or unzipped) file path given the unzipped version. If no path exists, returns the filename unmodified. * **Parameters** **filename** – filename without zip extension * **Returns** filename with a zip extension (unless an unzipped version exists). If filename is not found, the same filename is returned unchanged.monty-2024.7.29/docs/monty.pprint.md000066400000000000000000000071541465166210500171570ustar00rootroot00000000000000--- layout: default title: monty.pprint.md nav_exclude: true --- # monty.pprint module Pretty printing functions. ## *class* monty.pprint.DisplayEcoder(\*, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, sort_keys=False, indent=None, separators=None, default=None) Bases: `JSONEncoder` Help convert dicts and objects to a format that can be displayed in notebooks Constructor for JSONEncoder, with sensible defaults. If skipkeys is false, then it is a TypeError to attempt encoding of keys that are not str, int, float or None. If skipkeys is True, such items are simply skipped. If ensure_ascii is true, the output is guaranteed to be str objects with all incoming non-ASCII characters escaped. If ensure_ascii is false, the output can contain non-ASCII characters. If check_circular is true, then lists, dicts, and custom encoded objects will be checked for circular references during encoding to prevent an infinite recursion (which would cause an RecursionError). Otherwise, no such check takes place. If allow_nan is true, then NaN, Infinity, and -Infinity will be encoded as such. This behavior is not JSON specification compliant, but is consistent with most JavaScript based encoders and decoders. Otherwise, it will be a ValueError to encode such floats. If sort_keys is true, then the output of dictionaries will be sorted by key; this is useful for regression tests to ensure that JSON serializations can be compared on a day-to-day basis. If indent is a non-negative integer, then JSON array elements and object members will be pretty-printed with that indent level. An indent level of 0 will only insert newlines. None is the most compact representation. If specified, separators should be an (item_separator, key_separator) tuple. The default is (’, ‘, ‘: ‘) if *indent* is `None` and (‘,’, ‘: ‘) otherwise. To get the most compact JSON representation, you should specify (‘,’, ‘:’) to eliminate whitespace. If specified, default is a function that gets called for objects that can’t otherwise be serialized. It should return a JSON encodable version of the object or raise a `TypeError`. ### default(o) Try diffent ways of converting the present object for displaying ## monty.pprint.draw_tree(node, child_iter=>, text_str=>) * **Parameters** * **node** – the root of the tree to be drawn, * **child_iter** – function that when called with a node, returns an iterable over all its children * **text_str** – turns a node into the text to be displayed in the tree. The default implementations of these two arguments retrieve the children by accessing node.children and simply use str(node) to convert a node to a string. The resulting tree is drawn into a buffer and returned as a string. Based on [https://pypi.python.org/pypi/asciitree/](https://pypi.python.org/pypi/asciitree/) ## monty.pprint.pprint_json(data) Display a tree-like object in a jupyter notebook. Allows for collapsible interactive interaction with data. * **Parameters** **data** – a dictionary or object Based on: [https://gist.github.com/jmmshn/d37d5a1be80a6da11f901675f195ca22](https://gist.github.com/jmmshn/d37d5a1be80a6da11f901675f195ca22) ## monty.pprint.pprint_table(table, out=<_io.TextIOWrapper name=’’ mode=’w’ encoding=’utf-8’>, rstrip=False) Prints out a table of data, padded for alignment Each row must have the same number of columns. * **Parameters** * **table** – The table to print. A list of lists. * **out** – Output stream (file-like object) * **rstrip** – if True, trailing withespaces are removed from the entries.monty-2024.7.29/docs/monty.re.md000066400000000000000000000022031465166210500162370ustar00rootroot00000000000000--- layout: default title: monty.re.md nav_exclude: true --- # monty.re module Helpful regex based functions. E.g., grepping. ## monty.re.regrep(filename, patterns, reverse=False, terminate_on_match=False, postprocess=) A powerful regular expression version of grep. * **Parameters** * **filename** (*str*) – Filename to grep. * **patterns** (*dict*) – A dict of patterns, e.g., {“energy”: r”energy\\(sigma->0\\)\\s+=\\s+([\\d-.]+)”}. * **reverse** (*bool*) – Read files in reverse. Defaults to false. Useful for large files, especially when used with terminate_on_match. * **terminate_on_match** (*bool*) – Whether to terminate when there is at least one match in each key in pattern. * **postprocess** (*callable*) – A post processing function to convert all matches. Defaults to str, i.e., no change. * **Returns** > {key1: [[[matches…], lineno], [[matches…], lineno], > ```none > [[matches…], lineno], …], > ``` > key2: …} For reverse reads, the lineno is given as a -ve number. Please note that 0-based indexing is used. * **Return type** A dict of the following formmonty-2024.7.29/docs/monty.serialization.md000066400000000000000000000036321465166210500205150ustar00rootroot00000000000000--- layout: default title: monty.serialization.md nav_exclude: true --- # monty.serialization module This module implements serialization support for common formats such as json and yaml. ## monty.serialization.dumpfn(obj, fn, \*args, fmt=None, \*\*kwargs) Dump to a json/yaml directly by filename instead of a File-like object. File may also be a BZ2 (“.BZ2”) or GZIP (“.GZ”, “.Z”) compressed file. For YAML, ruamel.yaml must be installed. The file type is automatically detected from the file extension (case insensitive). YAML is assumed if the filename contains “.yaml” or “.yml”. Msgpack is assumed if the filename contains “.mpk”. JSON is otherwise assumed. * **Parameters** * **obj** (*object*) – Object to dump. * **fn** (*str/Path*) – filename or pathlib.Path. * **\*args** – Any of the args supported by json/yaml.dump. * **\*\*kwargs** – Any of the kwargs supported by json/yaml.dump. * **Returns** (object) Result of json.load. ## monty.serialization.loadfn(fn, \*args, fmt=None, \*\*kwargs) Loads json/yaml/msgpack directly from a filename instead of a File-like object. File may also be a BZ2 (“.BZ2”) or GZIP (“.GZ”, “.Z”) compressed file. For YAML, ruamel.yaml must be installed. The file type is automatically detected from the file extension (case insensitive). YAML is assumed if the filename contains “.yaml” or “.yml”. Msgpack is assumed if the filename contains “.mpk”. JSON is otherwise assumed. * **Parameters** * **fn** (*str/Path*) – filename or pathlib.Path. * **\*args** – Any of the args supported by json/yaml.load. * **fmt** (*string*) – If specified, the fmt specified would be used instead of autodetection from filename. Supported formats right now are “json”, “yaml” or “mpk”. * **\*\*kwargs** – Any of the kwargs supported by json/yaml.load. * **Returns** (object) Result of json/yaml/msgpack.load.monty-2024.7.29/docs/monty.shutil.md000066400000000000000000000050771465166210500171550ustar00rootroot00000000000000--- layout: default title: monty.shutil.md nav_exclude: true --- # monty.shutil module Copying and zipping utilities. Works on directories mostly. ## monty.shutil.compress_dir(path, compression=’gz’) Recursively compresses all files in a directory. Note that this compresses all files singly, i.e., it does not create a tar archive. For that, just use Python tarfile class. * **Parameters** * **path** (*str*) – Path to parent directory. * **compression** (*str*) – A compression mode. Valid options are “gz” or “bz2”. Defaults to gz. ## monty.shutil.compress_file(filepath, compression=’gz’) Compresses a file with the correct extension. Functions like standard Unix command line gzip and bzip2 in the sense that the original uncompressed files are not retained. * **Parameters** * **filepath** (*str*) – Path to file. * **compression** (*str*) – A compression mode. Valid options are “gz” or “bz2”. Defaults to “gz”. ## monty.shutil.copy_r(src, dst) Implements a recursive copy function similar to Unix’s “cp -r” command. Surprisingly, python does not have a real equivalent. shutil.copytree only works if the destination directory is not present. * **Parameters** * **src** (*str*) – Source folder to copy. * **dst** (*str*) – Destination folder. ## monty.shutil.decompress_dir(path) Recursively decompresses all files in a directory. * **Parameters** **path** (*str*) – Path to parent directory. ## monty.shutil.decompress_file(filepath) Decompresses a file with the correct extension. Automatically detects gz, bz2 or z extension. * **Parameters** **filepath** (*str*) – Path to file. * **Returns** The decompressed file path. * **Return type** str ## monty.shutil.gzip_dir(path, compresslevel=6) Gzips all files in a directory. Note that this is different from shutil.make_archive, which creates a tar archive. The aim of this method is to create gzipped files that can still be read using common Unix-style commands like zless or zcat. * **Parameters** * **path** (*str*) – Path to directory. * **compresslevel** (*int*) – Level of compression, 1-9. 9 is default for GzipFile, 6 is default for gzip. ## monty.shutil.remove(path, follow_symlink=False) Implements a remove function that will delete files, folder trees and symlink trees. 1.) Remove a file 2.) Remove a symlink and follow into with a recursive rm if follow_symlink 3.) Remove directory with rmtree * **Parameters** * **path** (*str*) – path to remove * **follow_symlink** (*bool*) – follow symlinks and removes whatever is in themmonty-2024.7.29/docs/monty.string.md000066400000000000000000000044431465166210500171470ustar00rootroot00000000000000--- layout: default title: monty.string.md nav_exclude: true --- # monty.string module Useful additional string functions. ## monty.string.boxed(msg, ch=’=’, pad=5) Returns a string in a box * **Parameters** * **msg** – Input string. * **ch** – Character used to form the box. * **pad** – Number of characters ch added before and after msg. ```python >>> print(boxed("hello", ch="*", pad=2)) *********** ** hello ** *********** ``` ## monty.string.indent(lines, amount, ch=’ ‘) Indent the lines in a string by padding each one with proper number of pad characters ## monty.string.is_string(s) True if s behaves like a string (duck typing test). ## monty.string.list_strings(arg) Always return a list of strings, given a string or list of strings as input. * **Examples** ```python >>> list_strings('A single string') ['A single string'] ``` ```python >>> list_strings(['A single string in a list']) ['A single string in a list'] ``` ```python >>> list_strings(['A','list','of','strings']) ['A', 'list', 'of', 'strings'] ``` ## monty.string.make_banner(s, width=78, mark=’\*’) * **Parameters** * **s** – String * **width** – Width of banner. Defaults to 78. * **mark** – The mark used to create the banner. * **Returns** Banner string. ## monty.string.marquee(text=’’, width=78, mark=’\*’) Return the input string centered in a ‘marquee’. * **Parameters** * **text** (*str*) – Input string * **width** (*int*) – Width of final output string. * **mark** (*str*) – Character used to fill string. * **Examples** ```python >>> marquee('A test', width=40) '**************** A test ****************' ``` ```python >>> marquee('A test', width=40, mark='-') '---------------- A test ----------------' ``` marquee(‘A test’,40, ‘ ‘) ‘ A test ‘ ## monty.string.remove_non_ascii(s) Remove non-ascii characters in a file. Needed when support for non-ASCII is not available. * **Parameters** **s** (*str*) – Input string * **Returns** String with all non-ascii characters removed. ## monty.string.unicode2str(s) Forces a unicode to a string in Python 2, but transparently handles Python 3. * **Parameters** **s** (*str/unicode*) – Input string / unicode. * **Returns** str in Python 2. Unchanged otherwise.monty-2024.7.29/docs/monty.subprocess.md000066400000000000000000000021101465166210500200160ustar00rootroot00000000000000--- layout: default title: monty.subprocess.md nav_exclude: true --- # monty.subprocess module Calling shell processes. ## *class* monty.subprocess.Command(command) Bases: `object` Enables to run subprocess commands in a different thread with TIMEOUT option. Based on jcollado’s solution: ```none [http://stackoverflow.com/questions/1191374/subprocess-with-timeout/4825933#4825933](http://stackoverflow.com/questions/1191374/subprocess-with-timeout/4825933#4825933) ``` and ```none [https://gist.github.com/kirpit/1306188](https://gist.github.com/kirpit/1306188) ``` ### retcode() Return code of the subprocess ### killed() True if subprocess has been killed due to the timeout ### output() stdout of the subprocess ### error() stderr of the subprocess ## Example com = Command(“sleep 1”).run(timeout=2) print(com.retcode, com.killed, com.output, com.output) * **Parameters** **command** – Command to execute ### run(timeout=None, \*\*kwargs) Run a command in a separated thread and wait timeout seconds. kwargs are keyword arguments passed to Popen. Return: selfmonty-2024.7.29/docs/monty.tempfile.md000066400000000000000000000053631465166210500174500ustar00rootroot00000000000000--- layout: default title: monty.tempfile.md nav_exclude: true --- # monty.tempfile module Temporary directory and file creation utilities. ## *class* monty.tempfile.ScratchDir(rootpath, create_symbolic_link=False, copy_from_current_on_enter=False, copy_to_current_on_exit=False, gzip_on_exit=False, delete_removed_files=True) Bases: `object` **NOTE**: With effect from Python 3.2, tempfile.TemporaryDirectory already implements much of the functionality of ScratchDir. However, it does not provide options for copying of files to and from (though it is possible to do this with other methods provided by shutil). Creates a “with” context manager that automatically handles creation of temporary directories (utilizing Python’s build in temp directory functions) and cleanup when done. This improves on Python’s built in functions by allowing for truly temporary workspace that are deleted when it is done. The way it works is as follows: 1. Create a temp dir in specified root path. 2. Optionally copy input files from current directory to temp dir. 3. Change to temp dir. 4. User performs specified operations. 5. Optionally copy generated output files back to original directory. 6. Change back to original directory. 7. Delete temp dir. Initializes scratch directory given a **root** path. There is no need to try to create unique directory names. The code will generate a temporary sub directory in the rootpath. The way to use this is using a with context manager. Example: ```default with ScratchDir("/scratch"): do_something() ``` If the root path does not exist or is None, this will function as a simple pass through, i.e., nothing happens. * **Parameters** * **rootpath** (*str/Path*) – Path in which to create temp subdirectories. If this is None, no temp directories will be created and this will just be a simple pass through. * **create_symbolic_link** (*bool*) – Whether to create a symbolic link in the current working directory to the scratch directory created. * **copy_from_current_on_enter** (*bool*) – Whether to copy all files from the current directory (recursively) to the temp dir at the start, e.g., if input files are needed for performing some actions. Defaults to False. * **copy_to_current_on_exit** (*bool*) – Whether to copy files from the scratch to the current directory (recursively) at the end. E .g., if output files are generated during the operation. Defaults to False. * **gzip_on_exit** (*bool*) – Whether to gzip the files generated in the ScratchDir before copying them back. Defaults to False. * **delete_removed_files** (*bool*) – Whether to delete files in the cwd that are removed from the tmp dir. Defaults to True ### SCR_LINK(_ = ‘scratch_link_ )monty-2024.7.29/docs/monty.termcolor.md000066400000000000000000000037311465166210500176460ustar00rootroot00000000000000--- layout: default title: monty.termcolor.md nav_exclude: true --- # monty.termcolor module Copyright (c) 2008-2011 Volvox Development Team # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the “Software”), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # Author: Konstantin Lepa <[konstantin.lepa@gmail.com](mailto:konstantin.lepa@gmail.com)> ANSII Color formatting for output in terminal. ## monty.termcolor.colored(text, color=None, on_color=None, attrs=None) Colorize text. Available text colors: ```none red, green, yellow, blue, magenta, cyan, white. ``` Available text highlights: ```none on_red, on_green, on_yellow, on_blue, on_magenta, on_cyan, on_white. ``` Available attributes: ```none bold, dark, underline, blink, reverse, concealed. ``` ## Example colored(‘Hello, World!’, ‘red’, ‘on_grey’, [‘blue’, ‘blink’]) colored(‘Hello, World!’, ‘green’) ## monty.termcolor.cprint(text, color=None, on_color=None, attrs=None, \*\*kwargs) Print colorize text. It accepts arguments of print function.monty-2024.7.29/pylintrc000066400000000000000000000475401465166210500150160ustar00rootroot00000000000000[MASTER] # Analyse import fallback blocks. This can be used to support both Python 2 and # 3 compatible code, which means that the block might have code that exists # only in one or another interpreter, leading to false positives when analysed. analyse-fallback-blocks=no # Load and enable all available extensions. Use --list-extensions to see a list # all available extensions. #enable-all-extensions= # In error mode, checkers without error messages are disabled and for others, # only the ERROR messages are displayed, and no reports are done by default. #errors-only= # Always return a 0 (non-error) status code, even if lint errors are found. # This is primarily useful in continuous integration scripts. #exit-zero= # A comma-separated list of package or module names from where C extensions may # be loaded. Extensions are loading into the active Python interpreter and may # run arbitrary code. extension-pkg-allow-list= # A comma-separated list of package or module names from where C extensions may # be loaded. Extensions are loading into the active Python interpreter and may # run arbitrary code. (This is an alternative name to extension-pkg-allow-list # for backward compatibility.) extension-pkg-whitelist= # Return non-zero exit code if any of these messages/categories are detected, # even if score is above --fail-under value. Syntax same as enable. Messages # specified are enabled, while categories only check already-enabled messages. fail-on= # Specify a score threshold to be exceeded before program exits with error. fail-under=10 # Interpret the stdin as a python script, whose filename needs to be passed as # the module_or_package argument. #from-stdin= # Files or directories to be skipped. They should be base names, not paths. ignore=CVS,tests,chemenv,abinit,defects # Add files or directories matching the regex patterns to the ignore-list. The # regex matches against paths and can be in Posix or Windows format. ignore-paths= # Files or directories matching the regex patterns are skipped. The regex # matches against base names, not paths. The default value ignores Emacs file # locks ignore-patterns=^\.# # List of module names for which member attributes should not be checked # (useful for modules/projects where namespaces are manipulated during runtime # and thus existing member attributes cannot be deduced by static analysis). It # supports qualified module names, as well as Unix pattern matching. ignored-modules= # Python code to execute, usually for sys.path manipulation such as # pygtk.require(). #init-hook= # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the # number of processors available to use. jobs=1 # Control the amount of potential inferred values when inferring a single # object. This can help the performance when dealing with large functions or # complex, nested conditions. limit-inference-results=100 # List of plugins (as comma separated values of python module names) to load, # usually to register additional checkers. load-plugins= # Pickle collected data for later comparisons. persistent=yes # Minimum Python version to use for version dependent checks. Will default to # the version used to run pylint. py-version=3.9 # Discover python modules and packages in the file system subtree. recursive=no # When enabled, pylint would attempt to guess common misconfiguration and emit # user-friendly hints instead of false-positive error messages. suggestion-mode=yes # Allow loading of arbitrary C extensions. Extensions are imported into the # active Python interpreter and may run arbitrary code. unsafe-load-any-extension=no # In verbose mode, extra non-checker-related info will be displayed. #verbose= [REPORTS] # Python expression which should return a score less than or equal to 10. You # have access to the variables 'fatal', 'error', 'warning', 'refactor', # 'convention', and 'info' which contain the number of messages in each # category, as well as 'statement' which is the total number of statements # analyzed. This score is used by the global evaluation report (RP0004). evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)) # Template used to display messages. This is a python new-style format string # used to format the message information. See doc for all details. msg-template= # Set the output format. Available formats are text, parseable, colorized, json # and msvs (visual studio). You can also give a reporter class, e.g. # mypackage.mymodule.MyReporterClass. #output-format= # Tells whether to display a full report or only the messages. reports=no # Activate the evaluation score. score=yes [MESSAGES CONTROL] # Only show warnings with the listed confidence levels. Leave empty to show # all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, # UNDEFINED. confidence=HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, UNDEFINED # Disable the message, report, category or checker with the given id(s). You # can either give multiple identifiers separated by comma (,) or put this # option multiple times (only on the command line, not in the configuration # file where it should appear only once). You can also use "--disable=all" to # disable everything first and then re-enable specific checks. For example, if # you want to run only the similarities checker, you can use "--disable=all # --enable=similarities". If you want to run only the classes checker, but have # no Warning level messages displayed, use "--disable=all --enable=classes # --disable=W". disable=raw-checker-failed, bad-inline-option, locally-disabled, file-ignored, suppressed-message, useless-suppression, deprecated-pragma, use-symbolic-message-instead, C0103, W, R, E1120, E1123, C0201, E0401, E0611, C0415, C0114, C0115, C0116, C0302, # C0209 # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option # multiple time (only on the command line, not in the configuration file where # it should appear only once). See also the "--disable" option for examples. enable=c-extension-no-member [LOGGING] # The type of string formatting that logging methods do. `old` means using % # formatting, `new` is for `{}` formatting. logging-format-style=old # Logging modules to check that the string format arguments are in logging # function parameter format. logging-modules=logging [SPELLING] # Limits count of emitted suggestions for spelling mistakes. max-spelling-suggestions=4 # Spelling dictionary name. Available dictionaries: none. To make it work, # install the 'python-enchant' package. spelling-dict= # List of comma separated words that should be considered directives if they # appear at the beginning of a comment and should not be checked. spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: # List of comma separated words that should not be checked. spelling-ignore-words= # A path to a file that contains the private dictionary; one word per line. spelling-private-dict-file= # Tells whether to store unknown words to the private dictionary (see the # --spelling-private-dict-file option) instead of raising a message. spelling-store-unknown-words=no [MISCELLANEOUS] # List of note tags to take in consideration, separated by a comma. notes=FIXME, XXX, TODO # Regular expression of note tags to take in consideration. notes-rgx= [TYPECHECK] # List of decorators that produce context managers, such as # contextlib.contextmanager. Add to this list to register other decorators that # produce valid context managers. contextmanager-decorators=contextlib.contextmanager # List of members which are set dynamically and missed by pylint inference # system, and so shouldn't trigger E1101 when accessed. Python regular # expressions are accepted. generated-members= # Tells whether to warn about missing members when the owner of the attribute # is inferred to be None. ignore-none=yes # This flag controls whether pylint should warn about no-member and similar # checks whenever an opaque object is returned when inferring. The inference # can return multiple potential results while evaluating a Python object, but # some branches might not be evaluated, which results in partial inference. In # that case, it might be useful to still emit no-member and other checks for # the rest of the inferred objects. ignore-on-opaque-inference=yes # List of symbolic message names to ignore for Mixin members. ignored-checks-for-mixins=no-member, not-async-context-manager, not-context-manager, attribute-defined-outside-init # List of class names for which member attributes should not be checked (useful # for classes with dynamically set attributes). This supports the use of # qualified names. ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace # Show a hint with possible names when a member name was not found. The aspect # of finding the hint is based on edit distance. missing-member-hint=yes # The minimum edit distance a name should have in order to be considered a # similar match for a missing member name. missing-member-hint-distance=1 # The total number of similar names that should be taken in consideration when # showing a hint for a missing member. missing-member-max-choices=1 # Regex pattern to define which classes are considered mixins. mixin-class-rgx=.*[Mm]ixin # List of decorators that change the signature of a decorated function. signature-mutators= [CLASSES] # Warn about protected attribute access inside special methods check-protected-access-in-special-methods=no # List of method names used to declare (i.e. assign) instance attributes. defining-attr-methods=__init__, __new__, setUp, __post_init__ # List of member names, which should be excluded from the protected access # warning. exclude-protected=_asdict, _fields, _replace, _source, _make # List of valid names for the first argument in a class method. valid-classmethod-first-arg=cls # List of valid names for the first argument in a metaclass class method. valid-metaclass-classmethod-first-arg=cls [VARIABLES] # List of additional names supposed to be defined in builtins. Remember that # you should avoid defining new builtins when possible. additional-builtins= # Tells whether unused global variables should be treated as a violation. allow-global-unused-variables=yes # List of names allowed to shadow builtins allowed-redefined-builtins= # List of strings which can identify a callback function by name. A callback # name must start or end with one of those strings. callbacks=cb_, _cb # A regular expression matching the name of dummy variables (i.e. expected to # not be used). dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ # Argument names that match this expression will be ignored. Default to name # with leading underscore. ignored-argument-names=_.*|^ignored_|^unused_ # Tells whether we should check for unused import in __init__ files. init-import=no # List of qualified module names which can have objects that can redefine # builtins. redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io [FORMAT] # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. expected-line-ending-format= # Regexp for a line that is allowed to be longer than the limit. ignore-long-lines=^\s*(# )??$ # Number of spaces of indent required inside a hanging or continued line. indent-after-paren=4 # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 # tab). indent-string=' ' # Maximum number of characters on a single line. max-line-length=120 # Maximum number of lines in a module. max-module-lines=1000 # Allow the body of a class to be on the same line as the declaration if body # contains single statement. single-line-class-stmt=no # Allow the body of an if to be on the same line as the test if there is no # else. single-line-if-stmt=no [IMPORTS] # List of modules that can be imported at any level, not just the top level # one. allow-any-import-level= # Allow wildcard imports from modules that define __all__. allow-wildcard-with-all=no # Deprecated modules which should not be used, separated by a comma. deprecated-modules= # Output a graph (.gv or any supported image format) of external dependencies # to the given file (report RP0402 must not be disabled). ext-import-graph= # Output a graph (.gv or any supported image format) of all (i.e. internal and # external) dependencies to the given file (report RP0402 must not be # disabled). import-graph= # Output a graph (.gv or any supported image format) of internal dependencies # to the given file (report RP0402 must not be disabled). int-import-graph= # Force import order to recognize a module as part of the standard # compatibility libraries. known-standard-library= # Force import order to recognize a module as part of a third party library. known-third-party=enchant # Couples of modules and preferred modules, separated by a comma. preferred-modules= [EXCEPTIONS] # Exceptions that will emit a warning when caught. overgeneral-exceptions=BaseException, Exception [REFACTORING] # Maximum number of nested blocks for function / method body max-nested-blocks=5 # Complete name of functions that never returns. When checking for # inconsistent-return-statements if a never returning function is called then # it will be considered as an explicit return statement and no message will be # printed. never-returning-functions=sys.exit,argparse.parse_error [SIMILARITIES] # Comments are removed from the similarity computation ignore-comments=yes # Docstrings are removed from the similarity computation ignore-docstrings=yes # Imports are removed from the similarity computation ignore-imports=yes # Signatures are removed from the similarity computation ignore-signatures=yes # Minimum lines number of a similarity. min-similarity-lines=4 [DESIGN] # List of regular expressions of class ancestor names to ignore when counting # public methods (see R0903) exclude-too-few-public-methods= # List of qualified class names to ignore when counting class parents (see # R0901) ignored-parents= # Maximum number of arguments for function / method. max-args=5 # Maximum number of attributes for a class (see R0902). max-attributes=7 # Maximum number of boolean expressions in an if statement (see R0916). max-bool-expr=5 # Maximum number of branch for function / method body. max-branches=12 # Maximum number of locals for function / method body. max-locals=15 # Maximum number of parents for a class (see R0901). max-parents=7 # Maximum number of public methods for a class (see R0904). max-public-methods=20 # Maximum number of return / yield for function / method body. max-returns=6 # Maximum number of statements in function / method body. max-statements=50 # Minimum number of public methods for a class (see R0903). min-public-methods=2 [STRING] # This flag controls whether inconsistent-quotes generates a warning when the # character used as a quote delimiter is used inconsistently within a module. check-quote-consistency=no # This flag controls whether the implicit-str-concat should generate a warning # on implicit string concatenation in sequences defined over several lines. check-str-concat-over-line-jumps=no [BASIC] # Naming style matching correct argument names. argument-naming-style=snake_case # Regular expression matching correct argument names. Overrides argument- # naming-style. If left empty, argument names will be checked with the set # naming style. #argument-rgx= # Naming style matching correct attribute names. attr-naming-style=snake_case # Regular expression matching correct attribute names. Overrides attr-naming- # style. If left empty, attribute names will be checked with the set naming # style. #attr-rgx= # Bad variable names which should always be refused, separated by a comma. bad-names=foo, bar, baz, toto, tutu, tata # Bad variable names regexes, separated by a comma. If names match any regex, # they will always be refused bad-names-rgxs= # Naming style matching correct class attribute names. class-attribute-naming-style=any # Regular expression matching correct class attribute names. Overrides class- # attribute-naming-style. If left empty, class attribute names will be checked # with the set naming style. #class-attribute-rgx= # Naming style matching correct class constant names. class-const-naming-style=UPPER_CASE # Regular expression matching correct class constant names. Overrides class- # const-naming-style. If left empty, class constant names will be checked with # the set naming style. #class-const-rgx= # Naming style matching correct class names. class-naming-style=PascalCase # Regular expression matching correct class names. Overrides class-naming- # style. If left empty, class names will be checked with the set naming style. #class-rgx= # Naming style matching correct constant names. const-naming-style=UPPER_CASE # Regular expression matching correct constant names. Overrides const-naming- # style. If left empty, constant names will be checked with the set naming # style. #const-rgx= # Minimum line length for functions/classes that require docstrings, shorter # ones are exempt. docstring-min-length=-1 # Naming style matching correct function names. function-naming-style=snake_case # Regular expression matching correct function names. Overrides function- # naming-style. If left empty, function names will be checked with the set # naming style. #function-rgx= # Good variable names which should always be accepted, separated by a comma. good-names=i, j, k, ex, Run, _ # Good variable names regexes, separated by a comma. If names match any regex, # they will always be accepted good-names-rgxs= # Include a hint for the correct naming format with invalid-name. include-naming-hint=no # Naming style matching correct inline iteration names. inlinevar-naming-style=any # Regular expression matching correct inline iteration names. Overrides # inlinevar-naming-style. If left empty, inline iteration names will be checked # with the set naming style. #inlinevar-rgx= # Naming style matching correct method names. method-naming-style=snake_case # Regular expression matching correct method names. Overrides method-naming- # style. If left empty, method names will be checked with the set naming style. #method-rgx= # Naming style matching correct module names. module-naming-style=snake_case # Regular expression matching correct module names. Overrides module-naming- # style. If left empty, module names will be checked with the set naming style. #module-rgx= # Colon-delimited sets of names that determine each other's naming style when # the name regexes allow several styles. name-group= # Regular expression which should only match function or class names that do # not require a docstring. no-docstring-rgx=^_ # List of decorators that produce properties, such as abc.abstractproperty. Add # to this list to register other decorators that produce valid properties. # These decorators are taken in consideration only for invalid-name. property-classes=abc.abstractproperty # Regular expression matching correct type variable names. If left empty, type # variable names will be checked with the set naming style. #typevar-rgx= # Naming style matching correct variable names. variable-naming-style=snake_case # Regular expression matching correct variable names. Overrides variable- # naming-style. If left empty, variable names will be checked with the set # naming style. #variable-rgx= monty-2024.7.29/pyproject.toml000066400000000000000000000036021465166210500161320ustar00rootroot00000000000000[build-system] requires = ["setuptools", "setuptools-scm"] build-backend = "setuptools.build_meta" [project] name = "monty" maintainers = [ { name = "Shyue Ping Ong", email = "ongsp@ucsd.edu" }, ] description = "Monty is the missing complement to Python." readme = "README.md" requires-python = ">=3.9" classifiers = [ "Programming Language :: Python :: 3", "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Topic :: Software Development :: Libraries :: Python Modules", ] dependencies = [ ] version = "2024.7.29" [project.optional-dependencies] ci = [ "pytest>=8", "pytest-cov>=4", "coverage", "numpy<2.0.0", "ruamel.yaml", "msgpack", "tqdm", "pymongo", "pandas", "pint", "orjson", "types-orjson", "types-requests", "torch" ] docs = [ "sphinx", "sphinx_rtd_theme", ] [tool.setuptools.packages.find] where = ["src"] include = ["monty"] [tool.black] line-length = 120 target-version = ['py39'] include = '\.pyi?$' exclude = ''' ( /( \.eggs # exclude a few common directories in the | \.git # root of the project | \.hg | \.mypy_cache | \.tox | \.venv | _build | buck-out | build | dist | tests )/ ) ''' [tool.coverage.run] branch = true [tool.coverage.report] exclude_also = [ "@deprecated", "def __repr__", "if 0:", "if __name__ == .__main__.:", "if self.debug:", "if settings.DEBUG", "pragma: no cover", "raise AssertionError", "raise NotImplementedError", "show_plot", "if TYPE_CHECKING:", "if typing.TYPE_CHECKING:", "except ImportError:" ] [tool.mypy] ignore_missing_imports = true [tool.ruff] lint.select = [ "I", #isort ] lint.isort.required-imports = ["from __future__ import annotations"] monty-2024.7.29/src/000077500000000000000000000000001465166210500140045ustar00rootroot00000000000000monty-2024.7.29/src/monty/000077500000000000000000000000001465166210500151525ustar00rootroot00000000000000monty-2024.7.29/src/monty/__init__.py000066400000000000000000000013121465166210500172600ustar00rootroot00000000000000""" Monty is the missing complement to Python. Monty implements supplementary useful functions for Python that are not part of the standard library. Examples include useful utilities like transparent support for zipped files, useful design patterns such as singleton and cached_class, and many more. """ from __future__ import annotations from importlib.metadata import PackageNotFoundError, version __author__ = "Shyue Ping Ong" __copyright__ = "Copyright 2014, The Materials Virtual Lab" __maintainer__ = "Shyue Ping Ong" __email__ = "ongsp@ucsd.edu" __date__ = "Oct 12 2020" try: __version__ = version("pymatgen") except PackageNotFoundError: # pragma: no cover # package is not installed pass monty-2024.7.29/src/monty/bisect.py000066400000000000000000000032641465166210500170020ustar00rootroot00000000000000""" Additional bisect functions. Taken from https://docs.python.org/2/library/bisect.html The above bisect() functions are useful for finding insertion points but can be tricky or awkward to use for common searching tasks. The functions show how to transform them into the standard lookups for sorted lists. """ from __future__ import annotations import bisect as bs from typing import TYPE_CHECKING if TYPE_CHECKING: from typing import Optional __author__ = "Matteo Giantomassi" __copyright__ = "Copyright 2013, The Materials Virtual Lab" __version__ = "0.1" __maintainer__ = "Matteo Giantomass" __email__ = "gmatteo@gmail.com" __date__ = "11/09/14" def index(a: list[float], x: float, atol: Optional[float] = None) -> int: """Locate the leftmost value exactly equal to x.""" i = bs.bisect_left(a, x) if i != len(a): if atol is None: if a[i] == x: return i elif abs(a[i] - x) < atol: return i raise ValueError def find_lt(a: list[float], x: float) -> int: """Find rightmost value less than x.""" if i := bs.bisect_left(a, x): return i - 1 raise ValueError def find_le(a: list[float], x: float) -> int: """Find rightmost value less than or equal to x.""" if i := bs.bisect_right(a, x): return i - 1 raise ValueError def find_gt(a: list[float], x: float) -> int: """Find leftmost value greater than x.""" i = bs.bisect_right(a, x) if i != len(a): return i raise ValueError def find_ge(a: list[float], x: float) -> int: """Find leftmost item greater than or equal to x.""" i = bs.bisect_left(a, x) if i != len(a): return i raise ValueError monty-2024.7.29/src/monty/collections.py000066400000000000000000000156121465166210500200470ustar00rootroot00000000000000""" Useful collection classes, e.g., tree, frozendict, etc. """ from __future__ import annotations import collections from typing import TYPE_CHECKING if TYPE_CHECKING: from typing import Any, Iterable from typing_extensions import Self def tree() -> collections.defaultdict: """ A tree object, which is effectively a recursive defaultdict that adds tree as members. Usage: x = tree() x['a']['b']['c'] = 1 Returns: A tree. """ return collections.defaultdict(tree) class frozendict(dict): """ A dictionary that does not permit changes. The naming violates PEP8 to be consistent with standard Python's "frozenset" naming. """ def __init__(self, *args, **kwargs) -> None: """ Args: args: Passthrough arguments for standard dict. kwargs: Passthrough keyword arguments for standard dict. """ dict.__init__(self, *args, **kwargs) def __setitem__(self, key: Any, val: Any) -> None: raise KeyError(f"Cannot overwrite existing key: {str(key)}") def update(self, *args, **kwargs) -> None: """ Args: args: Passthrough arguments for standard dict. kwargs: Passthrough keyword arguments for standard dict. """ raise KeyError(f"Cannot update a {self.__class__.__name__}") class Namespace(dict): """A dictionary that does not permit to redefine its keys.""" def __init__(self, *args, **kwargs) -> None: """ Args: args: Passthrough arguments for standard dict. kwargs: Passthrough keyword arguments for standard dict. """ self.update(*args, **kwargs) def __setitem__(self, key: Any, val: Any) -> None: if key in self: raise KeyError(f"Cannot overwrite existent key: {key!s}") dict.__setitem__(self, key, val) def update(self, *args, **kwargs) -> None: """ Args: args: Passthrough arguments for standard dict. kwargs: Passthrough keyword arguments for standard dict. """ for k, v in dict(*args, **kwargs).items(): self[k] = v class AttrDict(dict): """ Allows to access dict keys as obj.foo in addition to the traditional way obj['foo']" Examples: >>> d = AttrDict(foo=1, bar=2) >>> assert d["foo"] == d.foo >>> d.bar = "hello" >>> assert d.bar == "hello" """ def __init__(self, *args, **kwargs) -> None: """ Args: args: Passthrough arguments for standard dict. kwargs: Passthrough keyword arguments for standard dict. """ super().__init__(*args, **kwargs) self.__dict__ = self def copy(self) -> Self: """ Returns: Copy of AttrDict """ newd = super().copy() return self.__class__(**newd) class FrozenAttrDict(frozendict): """ A dictionary that: * does not permit changes. * Allows to access dict keys as obj.foo in addition to the traditional way obj['foo'] """ def __init__(self, *args, **kwargs) -> None: """ Args: args: Passthrough arguments for standard dict. kwargs: Passthrough keyword arguments for standard dict. """ super().__init__(*args, **kwargs) def __getattribute__(self, name: str) -> Any: try: return super().__getattribute__(name) except AttributeError: try: return self[name] except KeyError as exc: raise AttributeError(str(exc)) def __setattr__(self, name: str, value: Any) -> None: raise KeyError( f"You cannot modify attribute {name} of {self.__class__.__name__}" ) class MongoDict: """ This dict-like object allows one to access the entries in a nested dict as attributes. Entries (attributes) cannot be modified. It also provides Ipython tab completion hence this object is particularly useful if you need to analyze a nested dict interactively (e.g. documents extracted from a MongoDB database). >>> m = MongoDict({'a': {'b': 1}, 'x': 2}) >>> assert m.a.b == 1 and m.x == 2 >>> assert "a" in m and "b" in m.a >>> m["a"] {'b': 1} Notes: Cannot inherit from ABC collections.Mapping because otherwise dict.keys and dict.items will pollute the namespace. e.g MongoDict({"keys": 1}).keys would be the ABC dict method. """ def __init__(self, *args, **kwargs) -> None: """ Args: args: Passthrough arguments for standard dict. kwargs: Passthrough keyword arguments for standard dict. """ self.__dict__["_mongo_dict_"] = dict(*args, **kwargs) def __repr__(self) -> str: return str(self) def __str__(self) -> str: return str(self._mongo_dict_) def __setattr__(self, name: str, value: Any) -> None: raise NotImplementedError( f"You cannot modify attribute {name} of {self.__class__.__name__}" ) def __getattribute__(self, name: str) -> Any: try: return super().__getattribute__(name) except AttributeError: # raise try: a = self._mongo_dict_[name] if isinstance(a, collections.abc.Mapping): a = self.__class__(a) return a except Exception as exc: raise AttributeError(str(exc)) def __getitem__(self, slice_) -> Any: return self._mongo_dict_.__getitem__(slice_) def __iter__(self) -> Iterable: return iter(self._mongo_dict_) def __len__(self) -> int: return len(self._mongo_dict_) def __dir__(self) -> list: """ For Ipython tab completion. See http://ipython.org/ipython-doc/dev/config/integrating.html """ return sorted(k for k in self._mongo_dict_ if not callable(k)) def dict2namedtuple(*args, **kwargs) -> tuple: """ Helper function to create a class `namedtuple` from a dictionary. Examples: >>> t = dict2namedtuple(foo=1, bar="hello") >>> assert t.foo == 1 and t.bar == "hello" >>> t = dict2namedtuple([("foo", 1), ("bar", "hello")]) >>> assert t[0] == t.foo and t[1] == t.bar Warnings: - The order of the items in the namedtuple is not deterministic if kwargs are used. namedtuples, however, should always be accessed by attribute hence this behaviour should not represent a serious problem. - Don't use this function in code in which memory and performance are crucial since a dict is needed to instantiate the tuple! """ d = collections.OrderedDict(*args) d.update(**kwargs) return collections.namedtuple( typename="dict2namedtuple", field_names=list(d.keys()) )(**d) monty-2024.7.29/src/monty/design_patterns.py000066400000000000000000000071001465166210500207130ustar00rootroot00000000000000""" Some common design patterns such as singleton and cached classes. """ from __future__ import annotations import inspect import os from functools import wraps from typing import Any, Dict, Hashable, Tuple, TypeVar from weakref import WeakValueDictionary def singleton(cls): """ This decorator can be used to create a singleton out of a class. Usage: @singleton class MySingleton(): def __init__(): pass """ instances = {} def getinstance(): if cls not in instances: instances[cls] = cls() return instances[cls] return getinstance # https://github.com/microsoft/pylance-release/issues/3478 Klass = TypeVar("Klass") def cached_class(cls: type[Klass]) -> type[Klass]: """ Decorator to cache class instances by constructor arguments. This results in a class that behaves like a singleton for each set of constructor arguments, ensuring efficiency. Note that this should be used for *immutable classes only*. Having a cached mutable class makes very little sense. For efficiency, avoid using this decorator for situations where there are many constructor arguments permutations. If any arguments are non-hashable, that set of arguments is not cached. """ orig_new = cls.__new__ orig_init = cls.__init__ cache: WeakValueDictionary = WeakValueDictionary() @wraps(orig_new) def new_new(cls, *args: Any, **kwargs: Any) -> Any: # Normalize arguments sig = inspect.signature(orig_init) bound_args = sig.bind(None, *args, **kwargs) bound_args.apply_defaults() # Remove 'self' from the arguments normalized_args = tuple(bound_args.arguments.values())[1:] try: key = (cls, normalized_args) if key in cache: return cache[key] if orig_new is object.__new__: instance = orig_new(cls) else: instance = orig_new(cls, *args, **kwargs) orig_init(instance, *args, **kwargs) instance._initialized = True cache[key] = instance return instance except TypeError: # Can't cache this set of arguments if orig_new is object.__new__: instance = orig_new(cls) else: instance = orig_new(cls, *args, **kwargs) orig_init(instance, *args, **kwargs) instance._initialized = True return instance @wraps(orig_init) def new_init(self: Any, *args: Any, **kwargs: Any) -> None: if not hasattr(self, "_initialized"): orig_init(self, *args, **kwargs) self._initialized = True def reduce(self: Any) -> Tuple[type, Tuple, Dict[str, Any]]: for key, value in cache.items(): if value is self: cls, args = key return (cls, args, {}) raise ValueError("Instance not found in cache") cls.__new__ = new_new # type: ignore[method-assign] cls.__init__ = new_init # type: ignore[method-assign] cls.__reduce__ = reduce # type: ignore[method-assign] return cls class NullFile: """A file object that is associated to /dev/null.""" def __new__(cls): """ Pass through. """ return open(os.devnull, "w") # pylint: disable=R1732 def __init__(self): """no-op""" class NullStream: """A fake stream with a no-op write.""" def write(self, *args): # pylint: disable=E0211 """ Does nothing... """ monty-2024.7.29/src/monty/dev.py000066400000000000000000000177601465166210500163150ustar00rootroot00000000000000""" This module implements several useful functions and decorators that can be particularly useful for developers. E.g., deprecating methods / classes, etc. """ from __future__ import annotations import functools import inspect import logging import os import subprocess import sys import warnings from dataclasses import is_dataclass from datetime import datetime from typing import TYPE_CHECKING if TYPE_CHECKING: from typing import Callable, Optional, Type logger = logging.getLogger(__name__) def deprecated( replacement: Optional[Callable | str] = None, message: str = "", deadline: Optional[tuple[int, int, int]] = None, category: Type[Warning] = FutureWarning, ) -> Callable: """ Decorator to mark classes or functions as deprecated, with a possible replacement. Args: replacement (Callable | str): A replacement class or function. message (str): A warning message to be displayed. deadline (Optional[tuple[int, int, int]]): Optional deadline for removal of the old function/class, in format (yyyy, MM, dd). A CI warning would be raised after this date if is running in code owner' repo. category (Warning): Choose the category of the warning to issue. Defaults to FutureWarning. Another choice can be DeprecationWarning. Note that FutureWarning is meant for end users and is always shown unless silenced. DeprecationWarning is meant for developers and is never shown unless python is run in developmental mode or the filter is changed. Make the choice accordingly. Returns: Original function, but with a warning to use the updated function. """ def raise_deadline_warning() -> None: """Raise CI warning after removal deadline in code owner's repo.""" def _is_in_owner_repo() -> bool: """Check if is running in code owner's repo. Only generate reliable check when `git` is installed and remote name is "origin". """ try: # Get current running repo result = subprocess.run( ["git", "config", "--get", "remote.origin.url"], stdout=subprocess.PIPE, ) owner_repo = ( result.stdout.decode("utf-8") .strip() .lstrip("https://github.com/") # HTTPS clone .lstrip("git@github.com:") # SSH clone .rstrip(".git") # SSH clone ) return owner_repo == os.getenv("GITHUB_REPOSITORY") except (subprocess.CalledProcessError, FileNotFoundError): return False # Only raise warning in code owner's repo CI if ( _deadline is not None and os.getenv("CI") is not None and datetime.now() > _deadline and _is_in_owner_repo() ): raise DeprecationWarning( f"This function should have been removed on {_deadline:%Y-%m-%d}." ) def craft_message( old: Callable, replacement: Callable | str, message: str, deadline: datetime, ) -> str: msg = f"{old.__name__} is deprecated" if deadline is not None: msg += f", and will be removed on {_deadline:%Y-%m-%d}\n" if replacement is not None: if deadline is None: msg += "; use " # for better formatting else: msg += "Use " if isinstance(replacement, str): msg += f"{replacement} instead." else: if isinstance(replacement, property): r = replacement.fget elif isinstance(replacement, (classmethod, staticmethod)): r = replacement.__func__ else: r = replacement msg += f"{r.__name__} in {r.__module__} instead." if message: msg += "\n" + message return msg def deprecated_function_decorator(old: Callable) -> Callable: @functools.wraps(old) def wrapped(*args, **kwargs): msg = craft_message(old, replacement, message, _deadline) warnings.warn(msg, category=category, stacklevel=2) return old(*args, **kwargs) return wrapped def deprecated_class_decorator(cls: Type) -> Type: # Modify __post_init__ for dataclass if is_dataclass(cls) and hasattr(cls, "__post_init__"): original_init = cls.__post_init__ else: original_init = cls.__init__ @functools.wraps(original_init) def new_init(self, *args, **kwargs): msg = craft_message(cls, replacement, message, _deadline) warnings.warn(msg, category=category, stacklevel=2) original_init(self, *args, **kwargs) if is_dataclass(cls) and hasattr(cls, "__post_init__"): cls.__post_init__ = new_init else: cls.__init__ = new_init return cls # Convert deadline to datetime type _deadline = datetime(*deadline) if deadline is not None else None # Raise CI warning after removal deadline raise_deadline_warning() def decorator(target: Callable) -> Callable: if inspect.isfunction(target): return deprecated_function_decorator(target) elif inspect.isclass(target): return deprecated_class_decorator(target) else: raise TypeError( "The @deprecated decorator can only be applied to classes or functions" ) return decorator class requires: """ Decorator to mark classes or functions as requiring a specified condition to be true. This can be used to present useful error messages for optional dependencies. For example, decorating the following code will check if scipy is present and if not, a runtime error will be raised if someone attempts to call the use_scipy function: try: import scipy except ImportError: scipy = None @requires(scipy is not None, "scipy is not present.") def use_scipy(): print(scipy.majver) Args: condition: Condition necessary to use the class or function. message: A message to be displayed if the condition is not True. """ def __init__( self, condition: bool, message: str, err_cls: type[Exception] = RuntimeError ) -> None: """ Args: condition: A expression returning a bool. message: Message to display if condition is False. """ self.condition = condition self.message = message self.err_cls = err_cls def __call__(self, _callable: Callable) -> Callable: """ Args: _callable: Callable function. """ @functools.wraps(_callable) def decorated(*args, **kwargs): if not self.condition: raise self.err_cls(self.message) return _callable(*args, **kwargs) return decorated def install_excepthook(hook_type: str = "color", **kwargs) -> int: """ This function replaces the original python traceback with an improved version from Ipython. Use `color` for colourful traceback formatting, `verbose` for Ka-Ping Yee's "cgitb.py" version kwargs are the keyword arguments passed to the constructor. See IPython.core.ultratb.py for more info. Returns: 0 if hook is installed successfully. """ try: from IPython.core import ultratb # pylint: disable=import-outside-toplevel except ImportError: warnings.warn("Cannot install excepthook, IPyhon.core.ultratb not available") return 1 # Select the hook. hook = dict( color=ultratb.ColorTB, verbose=ultratb.VerboseTB, ).get(hook_type.lower(), None) if hook is None: return 2 sys.excepthook = hook(**kwargs) return 0 monty-2024.7.29/src/monty/fnmatch.py000066400000000000000000000027341465166210500171520ustar00rootroot00000000000000""" This module provides support for Unix shell-style wildcards """ from __future__ import annotations import fnmatch from monty.string import list_strings class WildCard: """ This object provides an easy-to-use interface for filename matching with shell patterns (fnmatch). >>> w = WildCard("*.nc|*.pdf") >>> w.filter(["foo.nc", "bar.pdf", "hello.txt"]) ['foo.nc', 'bar.pdf'] >>> w.filter("foo.nc") ['foo.nc'] """ def __init__(self, wildcard: str, sep: str = "|") -> None: """ Initializes a WildCard. Args: wildcard (str): String of tokens separated by sep. Each token represents a pattern. sep (str): Separator for shell patterns. """ self.pats = wildcard.split(sep) if wildcard else ["*"] def __str__(self) -> str: return f"<{self.__class__.__name__}, patterns = {self.pats}>" def filter(self, names: list[str]) -> list[str]: """ Returns a list with the names matching the pattern. """ names = list_strings(names) filenames = [] for filename in names: for pat in self.pats: if fnmatch.fnmatch(filename, pat): filenames.append(filename) return filenames def match(self, name: str) -> bool: """ Returns True if name matches one of the patterns. """ return any(fnmatch.fnmatch(name, pat) for pat in self.pats) monty-2024.7.29/src/monty/fractions.py000066400000000000000000000027201465166210500175150ustar00rootroot00000000000000""" Math functions. """ from __future__ import annotations import math from typing import Sequence def gcd(*numbers: int) -> int: """ Returns the greatest common divisor for a sequence of numbers. Args: *numbers: Sequence of numbers. Returns: int: Greatest common divisor of numbers. """ n: int = numbers[0] for i in numbers: n = math.gcd(n, i) return n def lcm(*numbers: int) -> int: """ Return lowest common multiple of a sequence of numbers. Args: *numbers: Sequence of numbers. Returns: int: Lowest common multiple of numbers. """ n = 1 for i in numbers: n = (i * n) // gcd(i, n) return n def gcd_float(numbers: Sequence[float], tol: float = 1e-8) -> float: """ Returns the greatest common divisor for a sequence of numbers. Uses a numerical tolerance, so can be used on floats Args: numbers: Sequence of numbers. tol: Numerical tolerance Returns: float: Greatest common divisor of numbers. """ def pair_gcd_tol(a: float, b: float) -> float: """Calculate the Greatest Common Divisor of a and b. Unless b==0, the result will have the same sign as b (so that when b is divided by it, the result comes out positive). """ while b > tol: a, b = b, a % b return a n = numbers[0] for i in numbers: n = pair_gcd_tol(n, i) return n monty-2024.7.29/src/monty/functools.py000066400000000000000000000202171465166210500175420ustar00rootroot00000000000000""" functools, especially backported from Python 3. """ from __future__ import annotations import cProfile import pstats import signal import sys import tempfile from collections import namedtuple from functools import partial, wraps from typing import Any, Callable, Union _CacheInfo = namedtuple("_CacheInfo", ["hits", "misses", "maxsize", "currsize"]) class _HashedSeq(list): # pylint: disable=C0205 """ This class guarantees that hash() will be called no more than once per element. This is important because the lru_cache() will hash the key multiple times on a cache miss. """ __slots__ = "hashvalue" def __init__(self, tup: tuple, hashfunc: Callable = hash) -> None: """ Args: tup: Tuple. hashfunc: Hash function. """ self[:] = tup self.hashvalue: int = hashfunc(tup) def __hash__(self) -> int: # type: ignore[override] return self.hashvalue def _make_key( args: tuple, kwds: dict, typed: bool, kwd_mark: tuple[object] = (object(),), fasttypes=None, ) -> Any: """ Make a cache key from optionally typed positional and keyword arguments The key is constructed in a way that is flat as possible rather than as a nested structure that would take more memory. If there is only a single argument and its data type is known to cache its hash value, then that argument is returned without a wrapper. This saves space and improves lookup speed. """ key = args if kwds: sorted_items = sorted(kwds.items()) key += kwd_mark for item in sorted_items: key += item if typed: key += tuple(type(v) for v in args) if kwds: key += tuple(type(v) for k, v in sorted_items) elif len(key) == 1 and isinstance(key[0], fasttypes): return key[0] return _HashedSeq(key) class lazy_property: """ lazy_property descriptor Used as a decorator to create lazy attributes. Lazy attributes are evaluated on first use. """ def __init__(self, func: Callable) -> None: """ Args: func: Function to decorate. """ self.__func = func wraps(self.__func)(self) # type: ignore[arg-type] def __get__(self, inst: Any, inst_cls) -> Any: if inst is None: return self if not hasattr(inst, "__dict__"): raise AttributeError( f"'{inst_cls.__name__}' object has no attribute '__dict__'" ) name = self.__name__ # type: ignore[attr-defined] # pylint: disable=E1101 if name.startswith("__") and not name.endswith("__"): name = f"_{inst_cls.__name__}{name}" value = self.__func(inst) inst.__dict__[name] = value return value @classmethod def invalidate(cls, inst: object, name: str) -> None: """Invalidate a lazy attribute. This obviously violates the lazy contract. A subclass of lazy may however have a contract where invalidation is appropriate. """ inst_cls = inst.__class__ if not hasattr(inst, "__dict__"): raise AttributeError( f"'{inst_cls.__name__}' object has no attribute '__dict__'" ) if name.startswith("__") and not name.endswith("__"): name = f"_{inst_cls.__name__}{name}" if not isinstance(getattr(inst_cls, name), cls): raise AttributeError( f"'{inst_cls.__name__}.{name}' is not a {cls.__name__} attribute" ) if name in inst.__dict__: del inst.__dict__[name] def return_if_raise( exception_tuple: Union[list, tuple], retval_if_exc: Any, disabled: bool = False ) -> Any: """ Decorator for functions, methods or properties. Execute the callable in a try block, and return retval_if_exc if one of the exceptions listed in exception_tuple is raised (se also ``return_node_if_raise``). Setting disabled to True disables the try except block (useful for debugging purposes). One can use this decorator to define properties. Examples: @return_if_raise(ValueError, None) def return_none_if_value_error(self): pass @return_if_raise((ValueError, KeyError), "hello") def another_method(self): pass @property @return_if_raise(AttributeError, None) def name(self): "Name of the object, None if not set." return self._name """ # we need a tuple of exceptions. if isinstance(exception_tuple, list): exception_tuple = tuple(exception_tuple) elif not isinstance(exception_tuple, tuple): exception_tuple = (exception_tuple,) else: raise TypeError(f"Wrong exception_tuple {type(exception_tuple)}") def decorator(func): @wraps(func) def wrapper(*args, **kwargs): if disabled: return func(*args, **kwargs) try: return func(*args, **kwargs) except exception_tuple: # pylint: disable=E0712 return retval_if_exc return wrapper return decorator # One could use None as default value in return_if_raise but this one is # explicit and more readable return_none_if_raise = partial(return_if_raise, retval_if_exc=None) """ This decorator returns None if one of the exceptions is raised. @return_none_if_raise(ValueError) def method(self): """ class timeout: """ Timeout function. Use to limit matching to a certain time limit. Note that this works only on Unix-based systems as it uses signal. Usage: try: with timeout(3): do_stuff() except TimeoutError: do_something_else() """ def __init__(self, seconds: int = 1, error_message: str = "Timeout"): """ Args: seconds (int): Allowed time for function in seconds. error_message (str): An error message. """ self.seconds = seconds self.error_message = error_message def handle_timeout(self, signum, frame): """ Args: signum: Return signal from call. frame: """ raise TimeoutError(self.error_message) def __enter__(self): signal.signal(signal.SIGALRM, self.handle_timeout) signal.alarm(self.seconds) def __exit__(self, type, value, traceback): signal.alarm(0) class TimeoutError(Exception): """ Exception class for timeouts. """ def __init__(self, message: str): """ Args: message: Error message """ self.message = message def prof_main(main): """ Decorator for profiling main programs. Profiling is activated by prepending the command line options supported by the original main program with the keyword `prof`. Examples: $ script.py arg --foo=1 becomes $ script.py prof arg --foo=1 The decorated main accepts two new arguments: prof_file: Name of the output file with profiling data If not given, a temporary file is created. sortby: Profiling data are sorted according to this value. default is "time". See sort_stats. """ @wraps(main) def wrapper(*args, **kwargs): try: do_prof = sys.argv[1] == "prof" if do_prof: sys.argv.pop(1) except Exception: do_prof = False if not do_prof: sys.exit(main()) else: print("Entering profiling mode...") prof_file = kwargs.get("prof_file", None) if prof_file is None: _, prof_file = tempfile.mkstemp() print(f"Profiling data stored in {prof_file}") sortby = kwargs.get("sortby", "time") cProfile.runctx("main()", globals(), locals(), prof_file) s = pstats.Stats(prof_file) s.strip_dirs().sort_stats(sortby).print_stats() if "retval" not in kwargs: sys.exit(0) else: return kwargs["retval"] return wrapper monty-2024.7.29/src/monty/inspect.py000066400000000000000000000042621465166210500171750ustar00rootroot00000000000000""" Useful additional functions to help get information about live objects """ from __future__ import annotations import inspect import os from inspect import currentframe, getframeinfo from typing import TYPE_CHECKING if TYPE_CHECKING: from typing import Literal, Type def all_subclasses(cls: Type) -> list[Type]: """ Given a class `cls`, this recursive function returns a list with all subclasses, subclasses of subclasses, and so on. """ subclasses = cls.__subclasses__() return subclasses + [g for s in subclasses for g in all_subclasses(s)] def find_top_pyfile(): """ This function inspects the Cpython frame to find the path of the script. """ frame = currentframe() while True: if frame.f_back is None: finfo = getframeinfo(frame) return os.path.abspath(finfo.filename) frame = frame.f_back def caller_name(skip: Literal[1, 2] = 2) -> str: """ Get a name of a caller in the format module.class.method `skip` specifies how many levels of stack to skip while getting caller name. skip=1 means "who calls me", skip=2 "who calls my caller" etc. An empty string is returned if skipped levels exceed stack height Taken from: https://gist.github.com/techtonik/2151727 Public Domain, i.e. feel free to copy/paste """ stack = inspect.stack() start = 0 + skip if len(stack) < start + 1: return "" parentframe = stack[start][0] name = [] # `modname` can be None when frame is executed directly in console # TODO(techtonik): consider using __main__ if module := inspect.getmodule(parentframe): name.append(module.__name__) # detect classname if "self" in parentframe.f_locals: # I don't know any way to detect call from the object method # XXX: there seems to be no way to detect static method call - it will # be just a function call name.append(parentframe.f_locals["self"].__class__.__name__) codename = parentframe.f_code.co_name if codename != "": # top level usually name.append(codename) # function or a method del parentframe return ".".join(name) monty-2024.7.29/src/monty/io.py000066400000000000000000000224001465166210500161310ustar00rootroot00000000000000""" Augments Python's suite of IO functions with useful transparent support for compressed files. """ from __future__ import annotations import bz2 import errno import gzip import io try: import lzma except ImportError: lzma = None # type: ignore[assignment] import mmap import os import subprocess import time from pathlib import Path from typing import TYPE_CHECKING if TYPE_CHECKING: from typing import IO, Generator, Union def zopen(filename: Union[str, Path], *args, **kwargs) -> IO: """ This function wraps around the bz2, gzip, lzma, xz and standard Python's open function to deal intelligently with bzipped, gzipped or standard text files. Args: filename (str/Path): filename or pathlib.Path. *args: Standard args for Python open(..). E.g., 'r' for read, 'w' for write. **kwargs: Standard kwargs for Python open(..). Returns: File-like object. Supports with context. """ if filename is not None and isinstance(filename, Path): filename = str(filename) _name, ext = os.path.splitext(filename) ext = ext.upper() if ext == ".BZ2": return bz2.open(filename, *args, **kwargs) if ext in {".GZ", ".Z"}: return gzip.open(filename, *args, **kwargs) if lzma is not None and ext in {".XZ", ".LZMA"}: return lzma.open(filename, *args, **kwargs) return open(filename, *args, **kwargs) def reverse_readfile(filename: Union[str, Path]) -> Generator[str, str, None]: """ A much faster reverse read of file by using Python's mmap to generate a memory-mapped file. It is slower for very small files than reverse_readline, but at least 2x faster for large files (the primary use of such a function). Args: filename (str | Path): File to read. Yields: Lines from the file in reverse order. """ try: with zopen(filename, "rb") as file: if isinstance(file, (gzip.GzipFile, bz2.BZ2File)): for line in reversed(file.readlines()): yield line.decode("utf-8").rstrip(os.linesep) else: filemap = mmap.mmap(file.fileno(), 0, access=mmap.ACCESS_READ) n = len(filemap) while n > 0: i = filemap.rfind(os.linesep.encode(), 0, n) yield filemap[i + 1 : n].decode("utf-8").rstrip(os.linesep) n = i except ValueError: return def reverse_readline( m_file, blk_size: int = 4096, max_mem: int = 4000000 ) -> Generator[str, str, None]: """ Generator function to read a file line-by-line, but backwards. This allows one to efficiently get data at the end of a file. Read file forwards and reverse in memory for files smaller than the max_mem parameter, or for gzip files where reverse seeks are not supported. Files larger than max_mem are dynamically read backwards. Reference: Based on code by Peter Astrand , using modifications by Raymond Hettinger and Kevin German. http://code.activestate.com/recipes/439045-read-a-text-file-backwards -yet-another-implementat/ Args: m_file (File): File stream to read (backwards) blk_size (int): The buffer size. Defaults to 4096. max_mem (int): The maximum amount of memory to involve in this operation. This is used to determine when to reverse a file in-memory versus seeking portions of a file. For bz2 files, this sets the maximum block size. Returns: Generator that yields lines from the file. Behave similarly to the file.readline() function, except the lines are returned from the back of the file. """ # Check if the file stream is a bit stream or not is_text = isinstance(m_file, io.TextIOWrapper) try: file_size = os.path.getsize(m_file.name) except AttributeError: # Bz2 files do not have name attribute. Just set file_size to above # max_mem for now. file_size = max_mem + 1 # If the file size is within our desired RAM use, just reverse it in memory # GZip files must use this method because there is no way to negative seek # For windows, we also read the whole file. if file_size < max_mem or isinstance(m_file, gzip.GzipFile) or os.name == "nt": for line in reversed(m_file.readlines()): yield line.rstrip() else: if isinstance(m_file, bz2.BZ2File): # for bz2 files, seeks are expensive. It is therefore in our best # interest to maximize the blk_size within limits of desired RAM # use. blk_size = min(max_mem, file_size) buf = "" m_file.seek(0, 2) lastchar = m_file.read(1) if is_text else m_file.read(1).decode("utf-8") trailing_newline = lastchar == os.linesep while True: newline_pos = buf.rfind(os.linesep) pos = m_file.tell() if newline_pos != -1: # Found a newline line = buf[newline_pos + 1 :] buf = buf[:newline_pos] if pos or newline_pos or trailing_newline: line += os.linesep yield line elif pos: # Need to fill buffer toread = min(blk_size, pos) m_file.seek(pos - toread, 0) if is_text: buf = m_file.read(toread) + buf else: buf = m_file.read(toread).decode("utf-8") + buf m_file.seek(pos - toread, 0) if pos == toread: buf = os.linesep + buf else: # Start-of-file return class FileLockException(Exception): """Exception raised by FileLock.""" class FileLock: """ A file locking mechanism that has context-manager support so you can use it in a with statement. This should be relatively cross-compatible as it doesn't rely on msvcrt or fcntl for the locking. Taken from http://www.evanfosmark.com/2009/01/cross-platform-file-locking -support-in-python/ """ Error = FileLockException def __init__( self, file_name: str, timeout: float = 10, delay: float = 0.05 ) -> None: """ Prepare the file locker. Specify the file to lock and optionally the maximum timeout and the delay between each attempt to lock. Args: file_name (str): Name of file to lock. timeout (float): Maximum timeout in second for locking. Defaults to 10. delay (float): Delay in second between each attempt to lock. Defaults to 0.05. """ self.file_name = os.path.abspath(file_name) self.lockfile = f"{os.path.abspath(file_name)}.lock" self.timeout = timeout self.delay = delay self.is_locked = False if self.delay > self.timeout or self.delay <= 0 or self.timeout <= 0: raise ValueError("delay and timeout must be positive with delay <= timeout") def __enter__(self): """ Activated when used in the with statement. Should automatically acquire a lock to be used in the with block. """ if not self.is_locked: self.acquire() return self def __exit__(self, type_, value, traceback): """ Activated at the end of the with statement. It automatically releases the lock if it isn't locked. """ if self.is_locked: self.release() def __del__(self): """ Make sure that the FileLock instance doesn't leave a lockfile lying around. """ self.release() def acquire(self) -> None: """ Acquire the lock, if possible. If the lock is in use, it check again every `delay` seconds. It does this until it either gets the lock or exceeds `timeout` number of seconds, in which case it throws an exception. """ start_time = time.time() while True: try: self.fd = os.open(self.lockfile, os.O_CREAT | os.O_EXCL | os.O_RDWR) break except OSError as exc: if exc.errno != errno.EEXIST: raise if (time.time() - start_time) >= self.timeout: raise FileLockException(f"{self.lockfile}: Timeout occurred.") time.sleep(self.delay) self.is_locked = True def release(self) -> None: """ Get rid of the lock by deleting the lockfile. When working in a `with` statement, this gets automatically called at the end. """ if self.is_locked: os.close(self.fd) os.unlink(self.lockfile) self.is_locked = False def get_open_fds() -> int: """ Get the number of open file descriptors for current process. Warnings: Will only work on UNIX-like OS-es. Returns: int: The number of open file descriptors for current process. """ pid: int = os.getpid() procs: bytes = subprocess.check_output(["lsof", "-w", "-Ff", "-p", str(pid)]) _procs: str = procs.decode("utf-8") return len([s for s in _procs.split("\n") if s and s[0] == "f" and s[1:].isdigit()]) monty-2024.7.29/src/monty/itertools.py000066400000000000000000000057071465166210500175610ustar00rootroot00000000000000""" Additional tools for iteration. """ from __future__ import annotations import itertools from typing import TYPE_CHECKING try: import numpy as np except ImportError: np = None if TYPE_CHECKING: from typing import Iterable def chunks(items: Iterable, n: int) -> Iterable: """ Yield successive n-sized chunks from a list-like object. >>> import pprint >>> pprint.pprint(list(chunks(range(1, 25), 10))) [(1, 2, 3, 4, 5, 6, 7, 8, 9, 10), (11, 12, 13, 14, 15, 16, 17, 18, 19, 20), (21, 22, 23, 24)] """ it = iter(items) chunk = tuple(itertools.islice(it, n)) while chunk: yield chunk chunk = tuple(itertools.islice(it, n)) def iterator_from_slice(s) -> Iterable: """ Constructs an iterator given a slice object s. Notes: The function returns an infinite iterator if s.stop is None """ start = s.start if s.start is not None else 0 step = s.step if s.step is not None else 1 if s.stop is None: # Infinite iterator. return itertools.count(start=start, step=step) # xrange-like iterator that supports float. return iter(np.arange(start, s.stop, step)) def iuptri( items: Iterable[Iterable], diago: bool = True, with_inds: bool = False ) -> Iterable[Iterable]: """ A generator that yields the upper triangle of the matrix (items x items) Args: items: Iterable object with elements [e0, e1, ...] diago: False if diagonal matrix elements should be excluded with_inds: If True, (i,j) (e_i, e_j) is returned else (e_i, e_j) >>> for (ij, mate) in iuptri([0,1], with_inds=True): ... print("ij:", ij, "mate:", mate) ij: (0, 0) mate: (0, 0) ij: (0, 1) mate: (0, 1) ij: (1, 1) mate: (1, 1) """ for ii, item1 in enumerate(items): for jj, item2 in enumerate(items): do_yield = (jj >= ii) if diago else (jj > ii) if do_yield: if with_inds: yield (ii, jj), (item1, item2) else: yield item1, item2 def ilotri( items: Iterable[Iterable], diago: bool = True, with_inds: bool = False ) -> Iterable[Iterable]: """ A generator that yields the lower triangle of the matrix (items x items) Args: items: Iterable object with elements [e0, e1, ...] diago: False if diagonal matrix elements should be excluded with_inds: If True, (i,j) (e_i, e_j) is returned else (e_i, e_j) >>> for (ij, mate) in ilotri([0,1], with_inds=True): ... print("ij:", ij, "mate:", mate) ij: (0, 0) mate: (0, 0) ij: (1, 0) mate: (1, 0) ij: (1, 1) mate: (1, 1) """ for ii, item1 in enumerate(items): for jj, item2 in enumerate(items): do_yield = (jj <= ii) if diago else (jj < ii) if do_yield: if with_inds: yield (ii, jj), (item1, item2) else: yield item1, item2 monty-2024.7.29/src/monty/json.py000066400000000000000000001061221465166210500164770ustar00rootroot00000000000000""" JSON serialization and deserialization utilities. """ from __future__ import annotations import dataclasses import datetime import json import os import pathlib import pickle import traceback import types from collections import OrderedDict, defaultdict from enum import Enum from hashlib import sha1 from importlib import import_module from inspect import getfullargspec from pathlib import Path from typing import Any from uuid import UUID, uuid4 try: import numpy as np except ImportError: np = None try: import pydantic except ImportError: pydantic = None try: from pydantic_core import core_schema except ImportError: core_schema = None try: import bson except ImportError: bson = None try: from ruamel.yaml import YAML except ImportError: YAML = None try: import orjson except ImportError: orjson = None try: import torch except ImportError: torch = None __version__ = "3.0.0" def _load_redirect(redirect_file): try: with open(redirect_file) as f: yaml = YAML() d = yaml.load(f) except OSError: # If we can't find the file # Just use an empty redirect dict return {} # Convert the full paths to module/class redirect_dict = defaultdict(dict) for old_path, new_path in d.items(): old_class = old_path.split(".")[-1] old_module = ".".join(old_path.split(".")[:-1]) new_class = new_path.split(".")[-1] new_module = ".".join(new_path.split(".")[:-1]) redirect_dict[old_module][old_class] = { "@module": new_module, "@class": new_class, } return dict(redirect_dict) def _check_type(obj, type_str) -> bool: """Alternative to isinstance that avoids imports. Checks whether obj is an instance of the type defined by type_str. This removes the need to explicitly import type_str. Handles subclasses like isinstance does. E.g.:: class A: pass class B(A): pass a, b = A(), B() assert isinstance(a, A) assert isinstance(b, B) assert isinstance(b, A) assert not isinstance(a, B) type_str: str | tuple[str] Note for future developers: the type_str is not always obvious for an object. For example, pandas.DataFrame is actually pandas.core.frame.DataFrame. To find out the type_str for an object, run type(obj).mro(). This will list all the types that an object can resolve to in order of generality (all objects have the builtins.object as the last one). """ type_str = type_str if isinstance(type_str, tuple) else (type_str,) # I believe this try-except is only necessary for callable types try: mro = type(obj).mro() except TypeError: return False return any(o.__module__ + "." + o.__name__ == ts for o in mro for ts in type_str) class MSONable: """ This is a mix-in base class specifying an API for msonable objects. MSON is Monty JSON. Essentially, MSONable objects must implement an as_dict method, which must return a json serializable dict and must also support no arguments (though optional arguments to finetune the output is ok), and a from_dict class method that regenerates the object from the dict generated by the as_dict method. The as_dict method should contain the "@module" and "@class" keys which will allow the MontyEncoder to dynamically deserialize the class. E.g.:: d["@module"] = self.__class__.__module__ d["@class"] = self.__class__.__name__ A default implementation is provided in MSONable, which automatically determines if the class already contains self.argname or self._argname attributes for every arg. If so, these will be used for serialization in the dict format. Similarly, the default from_dict will deserialization classes of such form. An example is given below:: class MSONClass(MSONable): def __init__(self, a, b, c, d=1, **kwargs): self.a = a self.b = b self._c = c self._d = d self.kwargs = kwargs For such classes, you merely need to inherit from MSONable and you do not need to implement your own as_dict or from_dict protocol. New to Monty V2.0.6.... Classes can be redirected to moved implementations by putting in the old fully qualified path and new fully qualified path into .monty.yaml in the home folder Example: old_module.old_class: new_module.new_class """ REDIRECT = _load_redirect(os.path.join(os.path.expanduser("~"), ".monty.yaml")) def as_dict(self) -> dict: """ A JSON serializable dict representation of an object. """ d: dict[str, Any] = { "@module": self.__class__.__module__, "@class": self.__class__.__name__, } try: parent_module = self.__class__.__module__.split(".", maxsplit=1)[0] module_version = import_module(parent_module).__version__ d["@version"] = str(module_version) except (AttributeError, ImportError): d["@version"] = None spec = getfullargspec(self.__class__.__init__) def recursive_as_dict(obj): if isinstance(obj, (list, tuple)): return [recursive_as_dict(it) for it in obj] if isinstance(obj, dict): return {kk: recursive_as_dict(vv) for kk, vv in obj.items()} if hasattr(obj, "as_dict"): return obj.as_dict() if dataclasses is not None and dataclasses.is_dataclass(obj): d = dataclasses.asdict(obj) d.update( { "@module": obj.__class__.__module__, "@class": obj.__class__.__name__, } ) return d return obj for c in spec.args + spec.kwonlyargs: if c != "self": try: a = getattr(self, c) except AttributeError: try: a = getattr(self, "_" + c) except AttributeError: raise NotImplementedError( "Unable to automatically determine as_dict " "format from class. MSONAble requires all " "args to be present as either self.argname or " "self._argname, and kwargs to be present under " "a self.kwargs variable to automatically " "determine the dict format. Alternatively, " "you can implement both as_dict and from_dict." ) d[c] = recursive_as_dict(a) if hasattr(self, "kwargs"): d.update(**self.kwargs) if spec.varargs is not None and getattr(self, spec.varargs, None) is not None: d.update({spec.varargs: getattr(self, spec.varargs)}) if hasattr(self, "_kwargs"): d.update(**self._kwargs) if isinstance(self, Enum): d.update({"value": self.value}) return d @classmethod def from_dict(cls, d): """ Args: d: Dict representation. Returns: MSONable class. """ decoded = { k: MontyDecoder().process_decoded(v) for k, v in d.items() if not k.startswith("@") } return cls(**decoded) def to_json(self) -> str: """ Returns a json string representation of the MSONable object. """ return json.dumps(self, cls=MontyEncoder) def unsafe_hash(self): """ Returns an hash of the current object. This uses a generic but low performance method of converting the object to a dictionary, flattening any nested keys, and then performing a hash on the resulting object """ def flatten(obj, separator="."): # Flattens a dictionary flat_dict = {} for key, value in obj.items(): if isinstance(value, dict): flat_dict.update( { separator.join([key, _key]): _value for _key, _value in flatten(value).items() } ) elif isinstance(value, list): list_dict = { f"{key}{separator}{num}": item for num, item in enumerate(value) } flat_dict.update(flatten(list_dict)) else: flat_dict[key] = value return flat_dict ordered_keys = sorted( flatten(jsanitize(self.as_dict())).items(), key=lambda x: x[0] ) ordered_keys = [item for item in ordered_keys if "@" not in item[0]] return sha1(json.dumps(OrderedDict(ordered_keys)).encode("utf-8")) @classmethod def _validate_monty(cls, __input_value): """ pydantic Validator for MSONable pattern """ if isinstance(__input_value, cls): return __input_value if isinstance(__input_value, dict): # Do not allow generic exceptions to be raised during deserialization # since pydantic may handle them incorrectly. try: new_obj = MontyDecoder().process_decoded(__input_value) if isinstance(new_obj, cls): return new_obj return cls(**__input_value) except Exception: raise ValueError( f"Error while deserializing {cls.__name__} " f"object: {traceback.format_exc()}" ) raise ValueError( f"Must provide {cls.__name__}, the as_dict form, or the proper" ) @classmethod def validate_monty_v1(cls, __input_value): """ Pydantic validator with correct signature for pydantic v1.x """ return cls._validate_monty(__input_value) @classmethod def validate_monty_v2(cls, __input_value, _): """ Pydantic validator with correct signature for pydantic v2.x """ return cls._validate_monty(__input_value) @classmethod def __get_validators__(cls): """Return validators for use in pydantic""" yield cls.validate_monty_v1 @classmethod def __get_pydantic_core_schema__(cls, source_type, handler): """ pydantic v2 core schema definition """ if core_schema is None: raise RuntimeError("Pydantic >= 2.0 is required for validation") s = core_schema.with_info_plain_validator_function(cls.validate_monty_v2) return core_schema.json_or_python_schema(json_schema=s, python_schema=s) @classmethod def _generic_json_schema(cls): return { "type": "object", "properties": { "@class": {"enum": [cls.__name__], "type": "string"}, "@module": {"enum": [cls.__module__], "type": "string"}, "@version": {"type": "string"}, }, "required": ["@class", "@module"], } @classmethod def __get_pydantic_json_schema__(cls, core_schema, handler): """JSON schema for MSONable pattern""" return cls._generic_json_schema() @classmethod def __modify_schema__(cls, field_schema): """JSON schema for MSONable pattern""" custom_schema = cls._generic_json_schema() field_schema.update(custom_schema) def _get_partial_json(self, json_kwargs, pickle_kwargs): """Used with the save method. Gets the json representation of a class with the unserializable components sustituted for hash references.""" if pickle_kwargs is None: pickle_kwargs = {} if json_kwargs is None: json_kwargs = {} encoder = MontyEncoder(allow_unserializable_objects=True, **json_kwargs) encoded = encoder.encode(self) return encoder, encoded, json_kwargs, pickle_kwargs def get_partial_json(self, json_kwargs=None, pickle_kwargs=None): """ Parameters ---------- json_kwargs : dict Keyword arguments to pass to the serializer. pickle_kwargs : dict Keyword arguments to pass to pickle.dump. Returns ------- str, dict The json encoding of the class and the name-object map if one is required, otherwise None. """ encoder, encoded, json_kwargs, pickle_kwargs = self._get_partial_json( json_kwargs, pickle_kwargs ) name_object_map = encoder._name_object_map if len(name_object_map) == 0: name_object_map = None return encoded, name_object_map, json_kwargs, pickle_kwargs def save( self, json_path, mkdir=True, json_kwargs=None, pickle_kwargs=None, strict=True, ): """Utility that uses the standard tools of MSONable to convert the class to json format, but also save it to disk. In addition, this method intelligently uses pickle to individually pickle class objects that are not serializable, saving them separately. This maximizes the readability of the saved class information while allowing _any_ class to be at least partially serializable to disk. For a fully MSONable class, only a class.json file will be saved to the location {save_dir}/class.json. For a partially MSONable class, additional information will be saved to the save directory at {save_dir}. This includes a pickled object for each attribute that e serialized. Parameters ---------- file_path : os.PathLike The file to which to save the json object. A pickled object of the same name but different extension might also be saved if the class is not entirely MSONable. mkdir : bool If True, makes the provided directory, including all parent directories. json_kwargs : dict Keyword arguments to pass to the serializer. pickle_kwargs : dict Keyword arguments to pass to pickle.dump. strict : bool If True, will not allow you to overwrite existing files. """ json_path = Path(json_path) save_dir = json_path.parent encoded, name_object_map, json_kwargs, pickle_kwargs = self.get_partial_json( json_kwargs, pickle_kwargs ) if mkdir: save_dir.mkdir(exist_ok=True, parents=True) # Define the pickle path pickle_path = save_dir / f"{json_path.stem}.pkl" # Check if the files exist and the strict parameter is True if strict and json_path.exists(): raise FileExistsError(f"strict is true and file {json_path} exists") if strict and pickle_path.exists(): raise FileExistsError(f"strict is true and file {pickle_path} exists") # Save the json file with open(json_path, "w") as outfile: outfile.write(encoded) # Save the pickle file if we have anything to save from the name_object_map if name_object_map is not None: with open(pickle_path, "wb") as f: pickle.dump(name_object_map, f, **pickle_kwargs) @classmethod def load(cls, file_path): """Loads a class from a provided json file. Parameters ---------- file_path : os.PathLike The json file to load from. Returns ------- MSONable An instance of the class being reloaded. """ d = _d_from_path(file_path) return cls.from_dict(d) def load(path): """Loads a json file that was saved using MSONable.save. Parameters ---------- path : os.PathLike Path to the json file to load. Returns ------- MSONable """ d = _d_from_path(path) module = d["@module"] klass = d["@class"] module = import_module(module) klass = getattr(module, klass) return klass.from_dict(d) def _d_from_path(file_path): json_path = Path(file_path) save_dir = json_path.parent pickle_path = save_dir / f"{json_path.stem}.pkl" with open(json_path, "r") as infile: d = json.loads(infile.read()) if pickle_path.exists(): name_object_map = pickle.load(open(pickle_path, "rb")) d = _recursive_name_object_map_replacement(d, name_object_map) return d def _recursive_name_object_map_replacement(d, name_object_map): if isinstance(d, dict): if "@object_reference" in d: name = d["@object_reference"] return name_object_map.pop(name) return { k: _recursive_name_object_map_replacement(v, name_object_map) for k, v in d.items() } elif isinstance(d, list): return [_recursive_name_object_map_replacement(x, name_object_map) for x in d] return d class MontyEncoder(json.JSONEncoder): """ A Json Encoder which supports the MSONable API, plus adds support for numpy arrays, datetime objects, bson ObjectIds (requires bson). Usage:: # Add it as a *cls* keyword when using json.dump json.dumps(object, cls=MontyEncoder) """ def __init__( self, *args, allow_unserializable_objects: bool = False, **kwargs ) -> None: super().__init__(*args, **kwargs) self._allow_unserializable_objects = allow_unserializable_objects self._name_object_map: dict[str, Any] = {} self._index: int = 0 def _update_name_object_map(self, o): name = f"{self._index:012}-{str(uuid4())}" self._index += 1 self._name_object_map[name] = o return {"@object_reference": name} def default(self, o) -> dict: """ Overriding default method for JSON encoding. This method does two things: (a) If an object has a to_dict property, return the to_dict output. (b) If the @module and @class keys are not in the to_dict, add them to the output automatically. If the object has no to_dict property, the default Python json encoder default method is called. Args: o: Python object. Return: Python dict representation. """ if isinstance(o, datetime.datetime): return { "@module": "datetime", "@class": "datetime", "string": str(o), } if isinstance(o, UUID): return {"@module": "uuid", "@class": "UUID", "string": str(o)} if isinstance(o, Path): return {"@module": "pathlib", "@class": "Path", "string": str(o)} if torch is not None and isinstance(o, torch.Tensor): # Support for Pytorch Tensors. d: dict[str, Any] = { "@module": "torch", "@class": "Tensor", "dtype": o.type(), } if "Complex" in o.type(): d["data"] = [o.real.tolist(), o.imag.tolist()] else: d["data"] = o.numpy().tolist() return d if np is not None: if isinstance(o, np.ndarray): if str(o.dtype).startswith("complex"): return { "@module": "numpy", "@class": "array", "dtype": str(o.dtype), "data": [o.real.tolist(), o.imag.tolist()], } return { "@module": "numpy", "@class": "array", "dtype": str(o.dtype), "data": o.tolist(), } if isinstance(o, np.generic): return o.item() if _check_type(o, "pandas.core.frame.DataFrame"): return { "@module": "pandas", "@class": "DataFrame", "data": o.to_json(default_handler=MontyEncoder().encode), } if _check_type(o, "pandas.core.series.Series"): return { "@module": "pandas", "@class": "Series", "data": o.to_json(default_handler=MontyEncoder().encode), } if _check_type(o, "pint.Quantity"): d = { "@module": "pint", "@class": "Quantity", "data": str(o), } try: module_version = import_module("pint").__version__ d["@version"] = str(module_version) except (AttributeError, ImportError): d["@version"] = None return d if bson is not None and isinstance(o, bson.objectid.ObjectId): return { "@module": "bson.objectid", "@class": "ObjectId", "oid": str(o), } if callable(o) and not isinstance(o, MSONable): try: return _serialize_callable(o) except AttributeError as e: # Some callables may not have instance __name__ if self._allow_unserializable_objects: return self._update_name_object_map(o) raise AttributeError(e) try: if pydantic is not None and isinstance(o, pydantic.BaseModel): d = o.model_dump() elif ( dataclasses is not None and (not issubclass(o.__class__, MSONable)) and dataclasses.is_dataclass(o) ): # This handles dataclasses that are not subclasses of MSONAble. d = dataclasses.asdict(o) # type: ignore[call-overload] elif hasattr(o, "as_dict"): d = o.as_dict() elif isinstance(o, Enum): d = {"value": o.value} elif self._allow_unserializable_objects: # Last resort logic. We keep track of some name of the object # as a reference, and instead of the object, store that # name, which of course is json-serializable d = self._update_name_object_map(o) else: raise TypeError( f"Object of type {o.__class__.__name__} is not JSON serializable" ) if "@module" not in d: d["@module"] = str(o.__class__.__module__) if "@class" not in d: d["@class"] = str(o.__class__.__name__) if "@version" not in d: try: parent_module = o.__class__.__module__.split(".")[0] module_version = import_module(parent_module).__version__ d["@version"] = str(module_version) except (AttributeError, ImportError): d["@version"] = None return d except AttributeError: return json.JSONEncoder.default(self, o) class MontyDecoder(json.JSONDecoder): """ A Json Decoder which supports the MSONable API. By default, the decoder attempts to find a module and name associated with a dict. If found, the decoder will generate a Pymatgen as a priority. If that fails, the original decoded dictionary from the string is returned. Note that nested lists and dicts containing pymatgen object will be decoded correctly as well. Usage: # Add it as a *cls* keyword when using json.load json.loads(json_string, cls=MontyDecoder) """ def process_decoded(self, d): """ Recursive method to support decoding dicts and lists containing pymatgen objects. """ if isinstance(d, dict): if "@module" in d and "@class" in d: modname = d["@module"] classname = d["@class"] if cls_redirect := MSONable.REDIRECT.get(modname, {}).get(classname): classname = cls_redirect["@class"] modname = cls_redirect["@module"] elif "@module" in d and "@callable" in d: modname = d["@module"] objname = d["@callable"] classname = None if d.get("@bound", None) is not None: # if the function is bound to an instance or class, first # deserialize the bound object and then remove the object name # from the function name. obj = self.process_decoded(d["@bound"]) objname = objname.split(".")[1:] else: # if the function is not bound to an object, import the # function from the module name obj = __import__(modname, globals(), locals(), [objname], 0) objname = objname.split(".") try: # the function could be nested. e.g., MyClass.NestedClass.function # so iteratively access the nesting for attr in objname: obj = getattr(obj, attr) return obj except AttributeError: pass else: modname = None classname = None if classname: if modname and modname not in { "bson.objectid", "numpy", "pandas", "pint", "torch", }: if modname == "datetime" and classname == "datetime": try: # Remove timezone info in the form of "+xx:00" dt = datetime.datetime.strptime( d["string"].split("+")[0], "%Y-%m-%d %H:%M:%S.%f" ) except ValueError: dt = datetime.datetime.strptime( d["string"].split("+")[0], "%Y-%m-%d %H:%M:%S" ) return dt elif modname == "uuid" and classname == "UUID": return UUID(d["string"]) elif modname == "pathlib" and classname == "Path": return Path(d["string"]) mod = __import__(modname, globals(), locals(), [classname], 0) if hasattr(mod, classname): cls_ = getattr(mod, classname) data = {k: v for k, v in d.items() if not k.startswith("@")} if hasattr(cls_, "from_dict"): return cls_.from_dict(data) if issubclass(cls_, Enum): return cls_(d["value"]) if pydantic is not None and issubclass( cls_, pydantic.BaseModel ): # pylint: disable=E1101 d = {k: self.process_decoded(v) for k, v in data.items()} return cls_(**d) if ( dataclasses is not None and (not issubclass(cls_, MSONable)) and dataclasses.is_dataclass(cls_) ): d = {k: self.process_decoded(v) for k, v in data.items()} return cls_(**d) elif torch is not None and modname == "torch" and classname == "Tensor": if "Complex" in d["dtype"]: return torch.tensor( # pylint: disable=E1101 [ np.array(r) + np.array(i) * 1j for r, i in zip(*d["data"]) ], ).type(d["dtype"]) return torch.tensor(d["data"]).type(d["dtype"]) # pylint: disable=E1101 elif np is not None and modname == "numpy" and classname == "array": if d["dtype"].startswith("complex"): return np.array( [ np.array(r) + np.array(i) * 1j for r, i in zip(*d["data"]) ], dtype=d["dtype"], ) return np.array(d["data"], dtype=d["dtype"]) elif modname == "pandas": import pandas as pd if classname == "DataFrame": decoded_data = MontyDecoder().decode(d["data"]) return pd.DataFrame(decoded_data) if classname == "Series": decoded_data = MontyDecoder().decode(d["data"]) return pd.Series(decoded_data) elif modname == "pint": from pint import UnitRegistry ureg = UnitRegistry() if classname == "Quantity": return ureg.Quantity(d["data"]) elif ( (bson is not None) and modname == "bson.objectid" and classname == "ObjectId" ): return bson.objectid.ObjectId(d["oid"]) return { self.process_decoded(k): self.process_decoded(v) for k, v in d.items() } if isinstance(d, list): return [self.process_decoded(x) for x in d] return d def decode(self, s): """ Overrides decode from JSONDecoder. :param s: string :return: Object. """ if orjson is not None: try: d = orjson.loads(s) # pylint: disable=E1101 except orjson.JSONDecodeError: # pylint: disable=E1101 d = json.loads(s) else: d = json.loads(s) return self.process_decoded(d) class MSONError(Exception): """ Exception class for serialization errors. """ def jsanitize( obj, strict=False, allow_bson=False, enum_values=False, recursive_msonable=False, ): """ This method cleans an input json-like object, either a list or a dict or some sequence, nested or otherwise, by converting all non-string dictionary keys (such as int and float) to strings, and also recursively encodes all objects using Monty's as_dict() protocol. Args: obj: input json-like object. strict (bool): This parameter sets the behavior when jsanitize encounters an object it does not understand. If strict is True, jsanitize will try to get the as_dict() attribute of the object. If no such attribute is found, an attribute error will be thrown. If strict is False, jsanitize will simply call str(object) to convert the object to a string representation. allow_bson (bool): This parameter sets the behavior when jsanitize encounters a bson supported type such as objectid and datetime. If True, such bson types will be ignored, allowing for proper insertion into MongoDB databases. enum_values (bool): Convert Enums to their values. recursive_msonable (bool): If True, uses .as_dict() for MSONables regardless of the value of strict. Returns: Sanitized dict that can be json serialized. """ if isinstance(obj, Enum): if enum_values: return obj.value elif hasattr(obj, "as_dict"): return obj.as_dict() return MontyEncoder().default(obj) if allow_bson and ( isinstance(obj, (datetime.datetime, bytes)) or (bson is not None and isinstance(obj, bson.objectid.ObjectId)) ): return obj if isinstance(obj, (list, tuple)): return [ jsanitize( i, strict=strict, allow_bson=allow_bson, enum_values=enum_values, recursive_msonable=recursive_msonable, ) for i in obj ] if np is not None and isinstance(obj, np.ndarray): try: return [ jsanitize( i, strict=strict, allow_bson=allow_bson, enum_values=enum_values, recursive_msonable=recursive_msonable, ) for i in obj.tolist() ] except TypeError: return obj.tolist() if np is not None and isinstance(obj, np.generic): return obj.item() if _check_type( obj, ( "pandas.core.series.Series", "pandas.core.frame.DataFrame", "pandas.core.base.PandasObject", ), ): return obj.to_dict() if isinstance(obj, dict): return { str(k): jsanitize( v, strict=strict, allow_bson=allow_bson, enum_values=enum_values, recursive_msonable=recursive_msonable, ) for k, v in obj.items() } if isinstance(obj, (int, float)): return obj if obj is None: return None if isinstance(obj, (pathlib.Path, datetime.datetime)): return str(obj) if callable(obj) and not isinstance(obj, MSONable): try: return _serialize_callable(obj) except TypeError: pass if recursive_msonable: try: return obj.as_dict() except AttributeError: pass if not strict: return str(obj) if isinstance(obj, str): return obj if pydantic is not None and isinstance(obj, pydantic.BaseModel): # pylint: disable=E1101 return jsanitize( MontyEncoder().default(obj), strict=strict, allow_bson=allow_bson, enum_values=enum_values, recursive_msonable=recursive_msonable, ) return jsanitize( obj.as_dict(), strict=strict, allow_bson=allow_bson, enum_values=enum_values, recursive_msonable=recursive_msonable, ) def _serialize_callable(o): if isinstance(o, types.BuiltinFunctionType): # don't care about what builtin functions (sum, open, etc) are bound to bound = None else: # bound methods (i.e., instance methods) have a __self__ attribute # that points to the class/module/instance bound = getattr(o, "__self__", None) # we are only able to serialize bound methods if the object the method is # bound to is itself serializable if bound is not None: try: bound = MontyEncoder().default(bound) except TypeError: raise TypeError( "Only bound methods of classes or MSONable instances are supported." ) return { "@module": o.__module__, "@callable": getattr(o, "__qualname__", o.__name__), "@bound": bound, } monty-2024.7.29/src/monty/logging.py000066400000000000000000000044441465166210500171600ustar00rootroot00000000000000""" Logging tools """ from __future__ import annotations import argparse import datetime import functools import logging from typing import TYPE_CHECKING if TYPE_CHECKING: from typing import Callable logger = logging.getLogger(__name__) def logged(level: int = logging.DEBUG) -> Callable: """ Useful logging decorator. If a method is logged, the beginning and end of the method call will be logged at a pre-specified level. Args: level: Level to log method at. Defaults to DEBUG. """ def wrap(f): _logger = logging.getLogger(f"{f.__module__}.{f.__name__}") def wrapped_f(*args, **kwargs): _logger.log( level, f"Called at {datetime.datetime.now()} with args = {args} and kwargs = {kwargs}", ) data = f(*args, **kwargs) _logger.log( level, f"Done at {datetime.datetime.now()} with args = {args} and kwargs = {kwargs}", ) return data return wrapped_f return wrap def enable_logging(main: Callable) -> Callable: """ This decorator is used to decorate main functions. It adds the initialization of the logger and an argument parser that allows one to select the loglevel. Useful if we are writing simple main functions that call libraries where the logging module is used Args: main: The main function. """ @functools.wraps(main) def wrapper(*args, **kwargs): parser = argparse.ArgumentParser() parser.add_argument( "--loglevel", default="ERROR", type=str, help="Set the loglevel. Possible values: CRITICAL, ERROR (default)," "WARNING, INFO, DEBUG", ) options = parser.parse_args() # loglevel is bound to the string value obtained from the command line # argument. # Convert to upper case to allow the user to specify --loglevel=DEBUG # or --loglevel=debug numeric_level = getattr(logging, options.loglevel.upper(), None) if not isinstance(numeric_level, int): raise ValueError(f"Invalid log level: {options.loglevel}") logging.basicConfig(level=numeric_level) return main(*args, **kwargs) return wrapper monty-2024.7.29/src/monty/math.py000066400000000000000000000011311465166210500164510ustar00rootroot00000000000000""" Addition math functions. """ from __future__ import annotations import math def nCr(n: int, r: int) -> int: """ Calculates nCr (binomial coefficient or "n choose r"). Args: n (int): total number of items. r (int): items to choose Returns: nCr. """ f = math.factorial return int(f(n) / f(r) / f(n - r)) def nPr(n: int, r: int) -> int: """ Calculates nPr. Args: n (int): total number of items. r (int): items to permute Returns: nPr. """ f = math.factorial return int(f(n) / f(n - r)) monty-2024.7.29/src/monty/msgpack.py000066400000000000000000000013441465166210500171530ustar00rootroot00000000000000""" msgpack serialization and deserialization utilities. Right now, this is a stub using monty.json encoder and decoders. The naming is just for clearer usage with msgpack's default and object_hook naming. """ from __future__ import annotations from monty.json import MontyDecoder, MontyEncoder def default(obj: object) -> dict: """ For use with msgpack.packb(obj, default=default). Supports Monty's as_dict protocol, numpy arrays and datetime. """ return MontyEncoder().default(obj) def object_hook(d: dict) -> object: """ For use with msgpack.unpackb(dict, object_hook=object_hook.). Supports Monty's as_dict protocol, numpy arrays and datetime. """ return MontyDecoder().process_decoded(d) monty-2024.7.29/src/monty/multiprocessing.py000066400000000000000000000022041465166210500207510ustar00rootroot00000000000000""" Multiprocessing utilities. """ from __future__ import annotations from multiprocessing import Pool from typing import Callable, Iterable try: from tqdm.autonotebook import tqdm except ImportError: tqdm = None def imap_tqdm(nprocs: int, func: Callable, iterable: Iterable, *args, **kwargs) -> list: """ A wrapper around Pool.imap. Creates a Pool with nprocs and then runs a f unction over an iterable with progress bar. Args: nprocs: Number of processes func: Callable iterable: Iterable of arguments. args: Passthrough to Pool.imap kwargs: Passthrough to Pool.imap Returns: Results of Pool.imap. """ if tqdm is None: raise ImportError("tqdm must be installed for this function.") data = [] with Pool(nprocs) as pool: try: n = len(iterable) # type: ignore[arg-type] except TypeError: n = None # type: ignore[arg-type] with tqdm(total=n) as prog_bar: for d in pool.imap(func, iterable, *args, **kwargs): prog_bar.update() data.append(d) return data monty-2024.7.29/src/monty/operator.py000066400000000000000000000014251465166210500173610ustar00rootroot00000000000000""" Useful additional functions for operators """ from __future__ import annotations import contextlib import operator from typing import TYPE_CHECKING if TYPE_CHECKING: from typing import Callable def operator_from_str(op: str) -> Callable: """ Return the operator associated to the given string `op`. raises: `KeyError` if invalid string. """ d = { "==": operator.eq, "!=": operator.ne, ">": operator.gt, ">=": operator.ge, "<": operator.lt, "<=": operator.le, "+": operator.add, "-": operator.sub, "*": operator.mul, "%": operator.mod, "^": operator.xor, } with contextlib.suppress(AttributeError): d["/"] = operator.truediv return d[op] monty-2024.7.29/src/monty/os/000077500000000000000000000000001465166210500155735ustar00rootroot00000000000000monty-2024.7.29/src/monty/os/__init__.py000066400000000000000000000026461465166210500177140ustar00rootroot00000000000000""" Os functions, e.g., cd, makedirs_p. """ from __future__ import annotations import errno import os from contextlib import contextmanager from typing import TYPE_CHECKING if TYPE_CHECKING: from pathlib import Path from typing import Generator, Union __author__ = "Shyue Ping Ong" __copyright__ = "Copyright 2013, The Materials Project" __version__ = "0.1" __maintainer__ = "Shyue Ping Ong" __email__ = "ongsp@ucsd.edu" __date__ = "1/24/14" @contextmanager def cd(path: Union[str, Path]) -> Generator: """ A Fabric-inspired cd context that temporarily changes directory for performing some tasks, and returns to the original working directory afterwards. E.g., with cd("/my/path/"): do_something() Args: path: Path to cd to. """ cwd = os.getcwd() os.chdir(path) try: yield finally: os.chdir(cwd) def makedirs_p(path: Union[str, Path], **kwargs) -> None: """ Wrapper for os.makedirs that does not raise an exception if the directory already exists, in the fashion of "mkdir -p" command. The check is performed in a thread-safe way Args: path: path of the directory to create kwargs: standard kwargs for os.makedirs """ try: os.makedirs(path, **kwargs) except OSError as exc: if exc.errno == errno.EEXIST and os.path.isdir(path): pass else: raise monty-2024.7.29/src/monty/os/path.py000066400000000000000000000065631465166210500171130ustar00rootroot00000000000000""" Path based methods, e.g., which, zpath, etc. """ from __future__ import annotations import os from typing import TYPE_CHECKING from monty.fnmatch import WildCard from monty.string import list_strings if TYPE_CHECKING: from typing import Callable, Literal, Optional, Union def zpath(filename: str) -> str: """ Returns an existing (zipped or unzipped) file path given the unzipped version. If no path exists, returns the filename unmodified. Args: filename: filename without zip extension Returns: filename with a zip extension (unless an unzipped version exists). If filename is not found, the same filename is returned unchanged. """ exts = ("", ".gz", ".GZ", ".bz2", ".BZ2", ".z", ".Z") for ext in exts: filename = filename.removesuffix(ext) for ext in exts: zfilename = f"{filename}{ext}" if os.path.exists(zfilename): return zfilename return filename def find_exts( top: str, exts: Union[str, list[str]], exclude_dirs: Optional[str] = None, include_dirs: Optional[str] = None, match_mode: Literal["basename", "abspath"] = "basename", ) -> list[str]: """ Find all files with the extension listed in `exts` that are located within the directory tree rooted at `top` (including top itself, but excluding '.' and '..') Args: top (str): Root directory exts (str or list of strings): List of extensions. exclude_dirs (str): Wildcards used to exclude particular directories. Can be concatenated via `|` include_dirs (str): Wildcards used to select particular directories. `include_dirs` and `exclude_dirs` are mutually exclusive match_mode (str): "basename" if match should be done on the basename. "abspath" for absolute path. Returns: list[str]: Absolute paths of the files. Examples: # Find all pdf and ps files starting from the current directory. find_exts(".", ("pdf", "ps")) # Find all pdf files, exclude hidden directories and dirs whose name # starts with `_` find_exts(".", "pdf", exclude_dirs="_*|.*") # Find all ps files, in the directories whose basename starts with # output. find_exts(".", "ps", include_dirs="output*")) """ exts = list_strings(exts) # Handle file! if os.path.isfile(top): return [os.path.abspath(top)] if any(top.endswith(ext) for ext in exts) else [] # Build shell-style wildcards. if exclude_dirs is not None: _exclude_dirs: WildCard = WildCard(exclude_dirs) if include_dirs is not None: _include_dirs: WildCard = WildCard(include_dirs) mangle_functions: dict[str, Callable[..., str]] = { "basename": os.path.basename, "abspath": os.path.abspath, } mangle: Callable[..., str] = mangle_functions[match_mode] # Assume directory paths = [] for dirpath, _dirnames, filenames in os.walk(top): dirpath = os.path.abspath(dirpath) if exclude_dirs and _exclude_dirs.match(mangle(dirpath)): continue if include_dirs and not _include_dirs.match(mangle(dirpath)): continue for filename in filenames: if any(filename.endswith(ext) for ext in exts): paths.append(os.path.join(dirpath, filename)) return paths monty-2024.7.29/src/monty/pprint.py000066400000000000000000000066301465166210500170450ustar00rootroot00000000000000""" Pretty printing functions. """ from __future__ import annotations import sys from io import StringIO from json import JSONEncoder, loads from typing import TYPE_CHECKING if TYPE_CHECKING: from typing import Callable, TextIO def pprint_table( table: list[list], out: TextIO = sys.stdout, rstrip: bool = False ) -> None: """ Prints out a table of data, padded for alignment Each row must have the same number of columns. Args: table: The table to print. A list of lists. out: Output stream (file-like object) rstrip: if True, trailing withespaces are removed from the entries. """ def max_width_col(table: list[list], col_idx: int) -> int: """Get the maximum width of the given column index.""" return max(len(row[col_idx]) for row in table) if rstrip: for row_idx, row in enumerate(table): table[row_idx] = [c.rstrip() for c in row] col_paddings = [] ncols = len(table[0]) for i in range(ncols): col_paddings.append(max_width_col(table, i)) for row in table: # left col out.write(row[0].ljust(col_paddings[0] + 1)) # rest of the cols for i in range(1, len(row)): col = row[i].rjust(col_paddings[i] + 2) out.write(col) out.write("\n") def draw_tree(node, child_iter=lambda n: n.children, text_str=str): """ Args: node: the root of the tree to be drawn, child_iter: function that when called with a node, returns an iterable over all its children text_str: turns a node into the text to be displayed in the tree. The default implementations of these two arguments retrieve the children by accessing node.children and simply use str(node) to convert a node to a string. The resulting tree is drawn into a buffer and returned as a string. Based on https://pypi.python.org/pypi/asciitree/ """ return _draw_tree(node, "", child_iter, text_str) def _draw_tree(node, prefix: str, child_iter: Callable, text_str: Callable): buf = StringIO() children = list(child_iter(node)) # check if root node if prefix: buf.write(prefix[:-3]) buf.write(" +--") buf.write(text_str(node)) buf.write("\n") for index, child in enumerate(children): sub_prefix = prefix + " " if index + 1 == len(children) else prefix + " |" buf.write(_draw_tree(child, sub_prefix, child_iter, text_str)) return buf.getvalue() class DisplayEcoder(JSONEncoder): """ Help convert dicts and objects to a format that can be displayed in notebooks """ def default(self, o): """ Try different ways of converting the present object for displaying """ try: return o.as_dict() except Exception: pass try: return o.__dict__ except Exception: pass try: return str(o) except Exception: pass return None def pprint_json(data): """ Display a tree-like object in a jupyter notebook. Allows for collapsible interactive interaction with data. Args: data: a dictionary or object Based on: https://gist.github.com/jmmshn/d37d5a1be80a6da11f901675f195ca22 """ from IPython.display import JSON, display # pylint: disable=C0415 display(JSON(loads(DisplayEcoder().encode(data)))) monty-2024.7.29/src/monty/py.typed000066400000000000000000000000001465166210500166370ustar00rootroot00000000000000monty-2024.7.29/src/monty/re.py000066400000000000000000000040511465166210500161320ustar00rootroot00000000000000""" Helpful regex based functions. E.g., grepping. """ from __future__ import annotations import collections import contextlib import re from typing import TYPE_CHECKING from monty.io import reverse_readfile, zopen if TYPE_CHECKING: from typing import Callable def regrep( filename: str, patterns: dict, reverse: bool = False, terminate_on_match: bool = False, postprocess: Callable = str, ) -> dict: r""" A powerful regular expression version of grep. Args: filename (str): Filename to grep. patterns (dict): A dict of patterns, e.g., {"energy": r"energy\\(sigma->0\\)\\s+=\\s+([\\d\\-\\.]+)"}. reverse (bool): Read files in reverse. Defaults to false. Useful for large files, especially when used with terminate_on_match. terminate_on_match (bool): Whether to terminate when there is at least one match in each key in pattern. postprocess (callable): A post processing function to convert all matches. Defaults to str, i.e., no change. Returns: A dict of the following form: {key1: [[[matches...], lineno], [[matches...], lineno], [[matches...], lineno], ...], key2: ...} For reverse reads, the lineno is given as a -ve number. Please note that 0-based indexing is used. """ compiled = {k: re.compile(v) for k, v in patterns.items()} matches = collections.defaultdict(list) gen = reverse_readfile(filename) if reverse else zopen(filename, "rt") for i, line in enumerate(gen): for k, p in compiled.items(): if m := p.search(line): matches[k].append( [[postprocess(g) for g in m.groups()], -i if reverse else i] ) if terminate_on_match and all(len(matches.get(k, [])) for k in compiled): break with contextlib.suppress(Exception): # Try to close open file handle. Pass if it is a generator. gen.close() # type: ignore[attr-defined] return matches monty-2024.7.29/src/monty/serialization.py000066400000000000000000000111301465166210500203750ustar00rootroot00000000000000""" This module implements serialization support for common formats such as json and yaml. """ from __future__ import annotations import json import os from typing import TYPE_CHECKING try: from ruamel.yaml import YAML except ImportError: YAML = None # type: ignore[arg-type] from monty.io import zopen from monty.json import MontyDecoder, MontyEncoder from monty.msgpack import default, object_hook try: import msgpack except ImportError: msgpack = None if TYPE_CHECKING: from pathlib import Path from typing import Any, Optional, Union def loadfn(fn: Union[str, Path], *args, fmt: Optional[str] = None, **kwargs) -> Any: """ Loads json/yaml/msgpack directly from a filename instead of a File-like object. File may also be a BZ2 (".BZ2") or GZIP (".GZ", ".Z") compressed file. For YAML, ruamel.yaml must be installed. The file type is automatically detected from the file extension (case insensitive). YAML is assumed if the filename contains ".yaml" or ".yml". Msgpack is assumed if the filename contains ".mpk". JSON is otherwise assumed. Args: fn (str/Path): filename or pathlib.Path. *args: Any of the args supported by json/yaml.load. fmt (string): If specified, the fmt specified would be used instead of autodetection from filename. Supported formats right now are "json", "yaml" or "mpk". **kwargs: Any of the kwargs supported by json/yaml.load. Returns: object: Result of json/yaml/msgpack.load. """ if fmt is None: basename = os.path.basename(fn).lower() if ".mpk" in basename: fmt = "mpk" elif any(ext in basename for ext in (".yaml", ".yml")): fmt = "yaml" else: fmt = "json" if fmt == "mpk": if msgpack is None: raise RuntimeError( "Loading of message pack files is not possible as msgpack-python is not installed." ) if "object_hook" not in kwargs: kwargs["object_hook"] = object_hook with zopen(fn, "rb") as fp: return msgpack.load(fp, *args, **kwargs) # pylint: disable=E1101 else: with zopen(fn, "rt") as fp: if fmt == "yaml": if YAML is None: raise RuntimeError("Loading of YAML files requires ruamel.yaml.") yaml = YAML() return yaml.load(fp, *args, **kwargs) if fmt == "json": if "cls" not in kwargs: kwargs["cls"] = MontyDecoder return json.load(fp, *args, **kwargs) raise TypeError(f"Invalid format: {fmt}") def dumpfn(obj: object, fn: Union[str, Path], *args, fmt=None, **kwargs) -> None: """ Dump to a json/yaml directly by filename instead of a File-like object. File may also be a BZ2 (".BZ2") or GZIP (".GZ", ".Z") compressed file. For YAML, ruamel.yaml must be installed. The file type is automatically detected from the file extension (case insensitive). YAML is assumed if the filename contains ".yaml" or ".yml". Msgpack is assumed if the filename contains ".mpk". JSON is otherwise assumed. Args: obj (object): Object to dump. fn (str/Path): filename or pathlib.Path. *args: Any of the args supported by json/yaml.dump. **kwargs: Any of the kwargs supported by json/yaml.dump. Returns: (object) Result of json.load. """ if fmt is None: basename = os.path.basename(fn).lower() if ".mpk" in basename: fmt = "mpk" elif any(ext in basename for ext in (".yaml", ".yml")): fmt = "yaml" else: fmt = "json" if fmt == "mpk": if msgpack is None: raise RuntimeError( "Loading of message pack files is not possible as msgpack-python is not installed." ) if "default" not in kwargs: kwargs["default"] = default with zopen(fn, "wb") as fp: msgpack.dump(obj, fp, *args, **kwargs) # pylint: disable=E1101 else: with zopen(fn, "wt") as fp: if fmt == "yaml": if YAML is None: raise RuntimeError("Loading of YAML files requires ruamel.yaml.") yaml = YAML() yaml.dump(obj, fp, *args, **kwargs) elif fmt == "json": if "cls" not in kwargs: kwargs["cls"] = MontyEncoder fp.write(json.dumps(obj, *args, **kwargs)) else: raise TypeError(f"Invalid format: {fmt}") monty-2024.7.29/src/monty/shutil.py000066400000000000000000000145701465166210500170430ustar00rootroot00000000000000"""Copying and zipping utilities. Works on directories mostly.""" from __future__ import annotations import os import shutil import warnings from gzip import GzipFile from pathlib import Path from typing import TYPE_CHECKING from monty.io import zopen if TYPE_CHECKING: from typing import Literal, Optional def copy_r(src: str | Path, dst: str | Path) -> None: """ Implements a recursive copy function similar to Unix's "cp -r" command. Surprisingly, python does not have a real equivalent. shutil.copytree only works if the destination directory is not present. Args: src (str | Path): Source folder to copy. dst (str | Path): Destination folder. """ src = Path(src) dst = Path(dst) abssrc = src.resolve() absdst = dst.resolve() os.makedirs(absdst, exist_ok=True) for filepath in os.listdir(abssrc): fpath = Path(abssrc, filepath) if fpath.is_symlink(): continue if fpath.is_file(): shutil.copy(fpath, absdst) elif str(fpath) not in str(absdst): copy_r(fpath, Path(absdst, filepath)) else: warnings.warn(f"Cannot copy {fpath} to itself") def gzip_dir(path: str | Path, compresslevel: int = 6) -> None: """ Gzips all files in a directory. Note that this is different from shutil.make_archive, which creates a tar archive. The aim of this method is to create gzipped files that can still be read using common Unix-style commands like zless or zcat. Args: path (str | Path): Path to directory. compresslevel (int): Level of compression, 1-9. 9 is default for GzipFile, 6 is default for gzip. """ path = Path(path) for root, _, files in os.walk(path): for f in files: full_f = Path(root, f).resolve() if Path(f).suffix.lower() != ".gz" and not full_f.is_dir(): with ( open(full_f, "rb") as f_in, GzipFile( f"{full_f}.gz", "wb", compresslevel=compresslevel ) as f_out, ): shutil.copyfileobj(f_in, f_out) shutil.copystat(full_f, f"{full_f}.gz") os.remove(full_f) def compress_file( filepath: str | Path, compression: Literal["gz", "bz2"] = "gz", target_dir: Optional[str | Path] = None, ) -> None: """ Compresses a file with the correct extension. Functions like standard Unix command line gzip and bzip2 in the sense that the original uncompressed files are not retained. Args: filepath (str | Path): Path to file. compression (str): A compression mode. Valid options are "gz" or "bz2". Defaults to "gz". target_dir (str | Path): An optional target dir where the result compressed file would be stored. Defaults to None for in-place compression. """ filepath = Path(filepath) target_dir = Path(target_dir) if target_dir is not None else None if compression not in {"gz", "bz2"}: raise ValueError("Supported compression formats are 'gz' and 'bz2'.") if filepath.suffix.lower() != f".{compression}" and not filepath.is_symlink(): if target_dir is not None: os.makedirs(target_dir, exist_ok=True) compressed_file: str | Path = target_dir / f"{filepath.name}.{compression}" else: compressed_file = f"{str(filepath)}.{compression}" with open(filepath, "rb") as f_in, zopen(compressed_file, "wb") as f_out: f_out.writelines(f_in) os.remove(filepath) def compress_dir(path: str | Path, compression: Literal["gz", "bz2"] = "gz") -> None: """ Recursively compresses all files in a directory. Note that this compresses all files singly, i.e., it does not create a tar archive. For that, just use Python tarfile class. Args: path (str | Path): Path to parent directory. compression (str): A compression mode. Valid options are "gz" or "bz2". Defaults to gz. """ path = Path(path) for parent, _, files in os.walk(path): for f in files: compress_file(Path(parent, f), compression=compression) return def decompress_file( filepath: str | Path, target_dir: Optional[str | Path] = None ) -> str | None: """ Decompresses a file with the correct extension. Automatically detects gz, bz2 or z extension. Args: filepath (str | Path): Path to file. target_dir (str | Path): An optional target dir where the result decompressed file would be stored. Defaults to None for in-place decompression. Returns: str | None: The decompressed file path, None if no operation. """ filepath = Path(filepath) target_dir = Path(target_dir) if target_dir is not None else None file_ext = filepath.suffix if file_ext.lower() in {".bz2", ".gz", ".z"} and filepath.is_file(): if target_dir is not None: os.makedirs(target_dir, exist_ok=True) decompressed_file: str | Path = target_dir / filepath.name.removesuffix( file_ext ) else: decompressed_file = str(filepath).removesuffix(file_ext) with zopen(filepath, "rb") as f_in, open(decompressed_file, "wb") as f_out: f_out.writelines(f_in) os.remove(filepath) return str(decompressed_file) return None def decompress_dir(path: str | Path) -> None: """ Recursively decompresses all files in a directory. Args: path (str | Path): Path to parent directory. """ path = Path(path) for parent, _, files in os.walk(path): for f in files: decompress_file(Path(parent, f)) def remove(path: str | Path, follow_symlink: bool = False) -> None: """ Implements a remove function that will delete files, folder trees and symlink trees. 1.) Remove a file 2.) Remove a symlink and follow into with a recursive rm if follow_symlink 3.) Remove directory with rmtree Args: path (str | Path): path to remove follow_symlink(bool): follow symlinks and removes whatever is in them """ path = Path(path) if path.is_file(): os.remove(path) elif path.is_symlink(): if follow_symlink: remove(os.readlink(path)) Path.unlink(path) else: shutil.rmtree(path) monty-2024.7.29/src/monty/string.py000066400000000000000000000064471465166210500170450ustar00rootroot00000000000000""" Useful additional string functions. """ from __future__ import annotations from typing import TYPE_CHECKING, Iterable, cast if TYPE_CHECKING: from typing import Any, Union def remove_non_ascii(s: str) -> str: """ Remove non-ascii characters in a file. Needed when support for non-ASCII is not available. Args: s (str): Input string Returns: String with all non-ascii characters removed. """ return "".join(i for i in s if ord(i) < 128) def is_string(s: Any) -> bool: """True if s behaves like a string (duck typing test).""" try: s + " " return True except TypeError: return False def list_strings(arg: Union[str, Iterable[str]]) -> list[str]: """ Always return a list of strings, given a string or list of strings as input. Examples: >>> list_strings('A single string') ['A single string'] >>> list_strings(['A single string in a list']) ['A single string in a list'] >>> list_strings(['A','list','of','strings']) ['A', 'list', 'of', 'strings'] >>> list_strings(('A','list','of','strings')) ['A', 'list', 'of', 'strings'] >>> list_strings({"a": 1, "b": 2}.keys()) ['a', 'b'] """ if is_string(arg): return [cast(str, arg)] return [cast(str, s) for s in arg] def marquee(text: str = "", width: int = 78, mark: str = "*") -> str: """ Return the input string centered in a 'marquee'. Args: text (str): Input string width (int): Width of final output string. mark (str): Character used to fill string. Examples: >>> marquee('A test', width=40) '**************** A test ****************' >>> marquee('A test', width=40, mark='-') '---------------- A test ----------------' marquee('A test',40, ' ') ' A test ' """ if not text: return (mark * width)[:width] nmark = (width - len(text) - 2) // len(mark) // 2 nmark = max(nmark, 0) marks = mark * nmark return f"{marks} {text} {marks}" def boxed(msg: str, ch: str = "=", pad: int = 5) -> str: """ Returns a string in a box Args: msg: Input string. ch: Character used to form the box. pad: Number of characters ch added before and after msg. Examples: >>> print(boxed("hello", ch="*", pad=2)) *********** ** hello ** *********** """ if pad > 0: msg = pad * ch + " " + msg.strip() + " " + pad * ch return "\n".join( [ len(msg) * ch, msg, len(msg) * ch, ] ) def make_banner(s: str, width: int = 78, mark: str = "*") -> str: """ Args: s: String width: Width of banner. Defaults to 78. mark: The mark used to create the banner. Returns: Banner string. """ banner = marquee(s, width=width, mark=mark) return "\n" + len(banner) * mark + "\n" + banner + "\n" + len(banner) * mark def indent(lines: str, amount: int, ch: str = " ") -> str: """ Indent the lines in a string by padding each one with proper number of pad characters """ padding = amount * ch return padding + ("\n" + padding).join(lines.split("\n")) monty-2024.7.29/src/monty/subprocess.py000066400000000000000000000054251465166210500177220ustar00rootroot00000000000000""" Calling shell processes. """ from __future__ import annotations import shlex import threading import traceback from subprocess import PIPE, Popen from typing import TYPE_CHECKING from monty.string import is_string if TYPE_CHECKING: from typing import Optional from typing_extensions import Self __author__ = "Matteo Giantomass" __copyright__ = "Copyright 2014, The Materials Virtual Lab" __version__ = "0.1" __maintainer__ = "Matteo Giantomassi" __email__ = "gmatteo@gmail.com" __date__ = "10/26/14" class Command: """ Enables to run subprocess commands in a different thread with TIMEOUT option. Based on jcollado's solution: http://stackoverflow.com/questions/1191374/subprocess-with-timeout/4825933#4825933 and https://gist.github.com/kirpit/1306188 Attributes: retcode: Return code of the subprocess killed: True if subprocess has been killed due to the timeout output: stdout of the subprocess error: stderr of the subprocess Examples: com = Command("sleep 1").run(timeout=2) print(com.retcode, com.killed, com.output, com.output) """ def __init__(self, command: str): """ Args: command: Command to execute """ if is_string(command): _command: list[str] = shlex.split(command) self.command = _command self.process = None self.retcode = None self.output, self.error = "", "" self.killed = False def __str__(self): return f"command: {self.command}, retcode: {self.retcode}" def run(self, timeout: Optional[float] = None, **kwargs) -> Self: """ Run a command in a separated thread and wait timeout seconds. kwargs are keyword arguments passed to Popen. Returns: self """ def target(**kw): try: # print('Thread started') with Popen(self.command, **kw) as self.process: self.output, self.error = self.process.communicate() self.retcode = self.process.returncode # print('Thread stopped') except Exception: self.error = traceback.format_exc() self.retcode = -1 # default stdout and stderr if "stdout" not in kwargs: kwargs["stdout"] = PIPE if "stderr" not in kwargs: kwargs["stderr"] = PIPE # thread thread = threading.Thread(target=target, kwargs=kwargs) thread.start() thread.join(timeout) if thread.is_alive() and self.process is not None: # print("Terminating process") self.process.terminate() self.killed = True thread.join() return self monty-2024.7.29/src/monty/tempfile.py000066400000000000000000000121231465166210500173300ustar00rootroot00000000000000""" Temporary directory and file creation utilities. """ from __future__ import annotations import os import tempfile from pathlib import Path from typing import TYPE_CHECKING from monty.shutil import copy_r, gzip_dir, remove if TYPE_CHECKING: from typing import Union class ScratchDir: """ Notes: With effect from Python 3.2, tempfile.TemporaryDirectory already implements much of the functionality of ScratchDir. However, it does not provide options for copying of files to and from (though it is possible to do this with other methods provided by shutil). Creates a "with" context manager that automatically handles creation of temporary directories (utilizing Python's build in temp directory functions) and cleanup when done. This improves on Python's built in functions by allowing for truly temporary workspace that are deleted when it is done. The way it works is as follows: 1. Create a temp dir in specified root path. 2. Optionally copy input files from current directory to temp dir. 3. Change to temp dir. 4. User performs specified operations. 5. Optionally copy generated output files back to original directory. 6. Change back to original directory. 7. Delete temp dir. """ SCR_LINK = "scratch_link" def __init__( self, rootpath: Union[str, Path, None], create_symbolic_link: bool = False, copy_from_current_on_enter: bool = False, copy_to_current_on_exit: bool = False, gzip_on_exit: bool = False, delete_removed_files: bool = True, ): """ Initializes scratch directory given a **root** path. There is no need to try to create unique directory names. The code will generate a temporary sub directory in the rootpath. The way to use this is using a with context manager. Example:: with ScratchDir("/scratch"): do_something() If the root path does not exist or is None, this will function as a simple pass through, i.e., nothing happens. Args: rootpath (str/Path): Path in which to create temp subdirectories. If this is None, no temp directories will be created and this will just be a simple pass through. create_symbolic_link (bool): Whether to create a symbolic link in the current working directory to the scratch directory created. copy_from_current_on_enter (bool): Whether to copy all files from the current directory (recursively) to the temp dir at the start, e.g., if input files are needed for performing some actions. Defaults to False. copy_to_current_on_exit (bool): Whether to copy files from the scratch to the current directory (recursively) at the end. E .g., if output files are generated during the operation. Defaults to False. gzip_on_exit (bool): Whether to gzip the files generated in the ScratchDir before copying them back. Defaults to False. delete_removed_files (bool): Whether to delete files in the cwd that are removed from the tmp dir. Defaults to True """ if Path is not None and isinstance(rootpath, Path): rootpath = str(rootpath) self.rootpath = os.path.abspath(rootpath) if rootpath is not None else None self.cwd = os.getcwd() self.create_symbolic_link = create_symbolic_link self.start_copy = copy_from_current_on_enter self.end_copy = copy_to_current_on_exit self.gzip_on_exit = gzip_on_exit self.delete_removed_files = delete_removed_files def __enter__(self): tempdir = self.cwd if self.rootpath is not None and os.path.exists(self.rootpath): tempdir = tempfile.mkdtemp(dir=self.rootpath) self.tempdir = os.path.abspath(tempdir) if self.start_copy: copy_r(self.cwd, tempdir) if self.create_symbolic_link: os.symlink(tempdir, ScratchDir.SCR_LINK) os.chdir(tempdir) return tempdir def __exit__(self, exc_type, exc_val, exc_tb): if self.rootpath is not None and os.path.exists(self.rootpath): if self.end_copy: files = set(os.listdir(self.tempdir)) orig_files = set(os.listdir(self.cwd)) # gzip files if self.gzip_on_exit: gzip_dir(self.tempdir) # copy files over copy_r(self.tempdir, self.cwd) # Delete any files that are now gone if self.delete_removed_files: for f in orig_files - files: fpath = os.path.join(self.cwd, f) remove(fpath) os.chdir(self.cwd) remove(self.tempdir) if self.create_symbolic_link and os.path.islink(ScratchDir.SCR_LINK): os.remove(ScratchDir.SCR_LINK) monty-2024.7.29/src/monty/termcolor.py000066400000000000000000000130421465166210500175320ustar00rootroot00000000000000""" Copyright (c) 2008-2011 Volvox Development Team # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # Author: Konstantin Lepa ANSII Color formatting for output in terminal. """ from __future__ import annotations import contextlib import os with contextlib.suppress(Exception): import curses import fcntl import struct import termios __all__ = ["colored", "cprint"] VERSION = (1, 1, 0) ATTRIBUTES = dict(bold=1, dark=2, underline=4, blink=5, reverse=7, concealed=8) HIGHLIGHTS = dict( on_grey=40, on_red=41, on_green=42, on_yellow=43, on_blue=44, on_magenta=45, on_cyan=46, on_white=47, ) COLORS = dict( grey=30, red=31, green=32, yellow=33, blue=34, magenta=35, cyan=36, white=37 ) RESET = "\033[0m" __ISON = True def enable(true_false: bool) -> None: """Enable/Disable ANSII Color formatting""" global __ISON __ISON = true_false def ison() -> bool: """True if ANSII Color formatting is activated.""" return __ISON def stream_has_colours(stream: object) -> bool: """ True if stream supports colours. Python cookbook, #475186 """ if not hasattr(stream, "isatty"): return False if not stream.isatty(): return False # auto color only on TTYs try: curses.setupterm() return curses.tigetnum("colors") > 2 except Exception: return False # guess false in case of error def colored( text: str, color: str = "", on_color: str = "", attrs: list[str] = [] ) -> str: """Colorize text. Available text colors: red, green, yellow, blue, magenta, cyan, white. Available text highlights: on_red, on_green, on_yellow, on_blue, on_magenta, on_cyan, on_white. Available attributes: bold, dark, underline, blink, reverse, concealed. Examples: colored('Hello, World!', 'red', 'on_grey', ['blue', 'blink']) colored('Hello, World!', 'green') """ if __ISON and os.getenv("ANSI_COLORS_DISABLED") is None: fmt_str = "\033[%dm%s" if color: text = fmt_str % (COLORS[color], text) if on_color: text = fmt_str % (HIGHLIGHTS[on_color], text) if attrs: for attr in attrs: text = fmt_str % (ATTRIBUTES[attr], text) text += RESET return text def cprint( text: str, color: str = "", on_color: str = "", attrs: list[str] = [], **kwargs ) -> None: """Print colorize text. It accepts arguments of print function. """ try: print((colored(text, color, on_color, attrs)), **kwargs) except TypeError: # flush is not supported by py2.7 kwargs.pop("flush", None) print((colored(text, color, on_color, attrs)), **kwargs) def colored_map(text: str, cmap: dict) -> str: """ Return colorized text. cmap is a dict mapping tokens to color options. colored_key("foo bar", {bar: "green"}) colored_key("foo bar", {bar: {"color": "green", "on_color": "on_red"}}) """ if not __ISON: return text for key, v in cmap.items(): if isinstance(v, dict): text = text.replace(key, colored(key, **v)) else: text = text.replace(key, colored(key, color=v)) return text def cprint_map(text: str, cmap: dict, **kwargs) -> None: """ Print colorize text. cmap is a dict mapping keys to color options. kwargs are passed to print function Examples: cprint_map("Hello world", {"Hello": "red"}) """ try: print(colored_map(text, cmap), **kwargs) except TypeError: # flush is not supported by py2.7 kwargs.pop("flush", None) print(colored_map(text, cmap), **kwargs) def get_terminal_size(): """ Return the size of the terminal as (nrow, ncols) Based on: http://stackoverflow.com/questions/566746/how-to-get-console-window- width-in-python """ with contextlib.suppress(Exception): rc = os.popen("stty size", "r").read().split() return int(rc[0]), int(rc[1]) env = os.environ def ioctl_GWINSZ(fd): try: return struct.unpack("hh", fcntl.ioctl(fd, termios.TIOCGWINSZ, "1234")) except Exception: return None rc = ioctl_GWINSZ(0) or ioctl_GWINSZ(1) or ioctl_GWINSZ(2) if not rc: with contextlib.suppress(Exception): fd = os.open(os.ctermid(), os.O_RDONLY) rc = ioctl_GWINSZ(fd) os.close(fd) if not rc: rc = (env.get("LINES", 25), env.get("COLUMNS", 80)) return int(rc[0]), int(rc[1]) monty-2024.7.29/tasks.py000077500000000000000000000104721465166210500147230ustar00rootroot00000000000000"""Deployment file to facilitate releases of monty.""" from __future__ import annotations import datetime import glob import json import os import re from typing import TYPE_CHECKING import requests from invoke import task from monty import __version__ as ver from monty.os import cd if TYPE_CHECKING: from invoke import Context __author__ = "Shyue Ping Ong" __copyright__ = "Copyright 2012, The Materials Project" __version__ = "0.1" __maintainer__ = "Shyue Ping Ong" __email__ = "shyue@mit.edu" __date__ = "Apr 29, 2012" NEW_VER = datetime.datetime.today().strftime("%Y.%-m.%-d") @task def make_doc(ctx: Context) -> None: with cd("docs"): ctx.run("rm monty.*.rst", warn=True) ctx.run("sphinx-apidoc --separate -P -M -d 6 -o . -f ../src/monty") # ctx.run("rm monty*.html", warn=True) # ctx.run("sphinx-build -b html . ../docs") # HTML building. ctx.run("sphinx-build -M markdown . .") ctx.run("rm *.rst", warn=True) ctx.run("cp markdown/monty*.md .") for fn in glob.glob("monty*.md"): with open(fn) as f: lines = [line.rstrip() for line in f if "Submodules" not in line] if fn == "monty.md": preamble = [ "---", "layout: default", "title: API Documentation", "nav_order: 5", "---", "", ] else: preamble = [ "---", "layout: default", f"title: {fn}", "nav_exclude: true", "---", "", ] with open(fn, "w") as f: f.write("\n".join(preamble + lines)) ctx.run("rm -r markdown", warn=True) ctx.run("cp ../*.md .") ctx.run("mv README.md index.md") ctx.run("rm -rf *.orig doctrees", warn=True) with open("index.md") as f: contents = f.read() with open("index.md", "w") as f: contents = re.sub( r"\n## Official Documentation[^#]*", "{: .no_toc }\n\n## Table of contents\n{: .no_toc .text-delta }\n* TOC\n{:toc}\n\n", contents, ) contents = ( "---\nlayout: default\ntitle: Home\nnav_order: 1\n---\n\n" + contents ) f.write(contents) @task def update_doc(ctx: Context) -> None: ctx.run("git pull", warn=True) make_doc(ctx) ctx.run("git add .", warn=True) ctx.run('git commit -a -m "Update dev docs"', warn=True) ctx.run("git push", warn=True) @task def test(ctx: Context) -> None: ctx.run("pytest") @task def setver(ctx: Context) -> None: ctx.run(f'sed s/version=.*,/version=\\"{ver}\\",/ setup.py > newsetup') ctx.run("mv newsetup setup.py") @task def release_github(ctx: Context) -> None: with open("docs/changelog.md") as f: contents = f.read() toks = re.split("##", contents) desc = toks[1].strip() payload = { "tag_name": f"v{NEW_VER}", "target_commitish": "master", "name": f"v{NEW_VER}", "body": desc, "draft": False, "prerelease": False, } response = requests.post( "https://api.github.com/repos/materialsvirtuallab/monty/releases", data=json.dumps(payload), headers={"Authorization": "token " + os.environ["GITHUB_RELEASES_TOKEN"]}, ) print(response.text) @task def commit(ctx: Context) -> None: ctx.run(f'git commit -a -m "v{NEW_VER} release"', warn=True) ctx.run("git push", warn=True) @task def set_ver(ctx: Context) -> None: with open("pyproject.toml", encoding="utf-8") as f: contents = f.read() contents = re.sub(r"version = ([\.\d\"]+)", f'version = "{NEW_VER}"', contents) with open("pyproject.toml", "w", encoding="utf-8") as f: f.write(contents) @task def release(ctx: Context, notest: bool = False) -> None: set_ver(ctx) if not notest: test(ctx) update_doc(ctx) commit(ctx) release_github(ctx) ctx.run("python -m build", warn=True) ctx.run("python -m build --wheel", warn=True) ctx.run("twine upload --skip-existing dist/*.whl", warn=True) ctx.run("twine upload --skip-existing dist/*.tar.gz", warn=True) monty-2024.7.29/tests/000077500000000000000000000000001465166210500143575ustar00rootroot00000000000000monty-2024.7.29/tests/__init__.py000066400000000000000000000004221465166210500164660ustar00rootroot00000000000000""" Unit tests and test files for Monty. """ from __future__ import annotations __author__ = "Shyue Ping Ong" __copyright__ = "Copyright 2012, The Materials Project" __version__ = "0.1" __maintainer__ = "Shyue Ping Ong" __email__ = "shyuep@gmail.com" __date__ = "1/24/14" monty-2024.7.29/tests/test_bisect.py000066400000000000000000000005621465166210500172440ustar00rootroot00000000000000from __future__ import annotations from monty.bisect import find_ge, find_gt, find_le, find_lt, index def test_funcs(): line = [0, 1, 2, 3, 4] assert index(line, 1) == 1 assert find_lt(line, 1) == 0 assert find_gt(line, 1) == 2 assert find_le(line, 1) == 1 assert find_ge(line, 2) == 2 # assert index([0, 1, 1.5, 2], 1.501, atol=0.1) == 4 monty-2024.7.29/tests/test_collections.py000066400000000000000000000026701465166210500203130ustar00rootroot00000000000000from __future__ import annotations import os import pytest from monty.collections import AttrDict, FrozenAttrDict, Namespace, frozendict, tree TEST_DIR = os.path.join(os.path.dirname(__file__), "test_files") class TestFrozenDict: def test_frozen_dict(self): d = frozendict({"hello": "world"}) with pytest.raises(KeyError): d["k"] == "v" assert d["hello"] == "world" def test_namespace_dict(self): d = Namespace(foo="bar") d["hello"] = "world" assert d["foo"] == "bar" with pytest.raises(KeyError): d.update({"foo": "spam"}) def test_attr_dict(self): d = AttrDict(foo=1, bar=2) assert d.bar == 2 assert d["foo"] == d.foo d.bar = "hello" assert d["bar"] == "hello" def test_frozen_attrdict(self): d = FrozenAttrDict({"hello": "world", 1: 2}) assert d["hello"] == "world" assert d.hello == "world" with pytest.raises(KeyError): d["updating"] == 2 with pytest.raises(KeyError): d["foo"] = "bar" with pytest.raises(KeyError): d.foo = "bar" with pytest.raises(KeyError): d.hello = "new" class TestTree: def test_tree(self): x = tree() x["a"]["b"]["c"]["d"] = 1 assert "b" in x["a"] assert "c" not in x["a"] assert "c" in x["a"]["b"] assert x["a"]["b"]["c"]["d"] == 1 monty-2024.7.29/tests/test_design_patterns.py000066400000000000000000000141321465166210500211620ustar00rootroot00000000000000from __future__ import annotations import gc import pickle import weakref from typing import Any import pytest from monty.design_patterns import cached_class, singleton class TestSingleton: def test_singleton(self): @singleton class A: pass a1 = A() a2 = A() assert id(a1) == id(a2) @cached_class class A: def __init__(self, val): self.val = val def __eq__(self, other): return self.val == other.val def __getinitargs__(self): return (self.val,) def __getnewargs__(self): return (self.val,) class TestCachedClass: def test_cached_class(self): a1a = A(1) a1b = A(1) a2 = A(2) assert id(a1a) == id(a1b) assert id(a1a) != id(a2) def test_pickle(self): a = A(2) o = pickle.dumps(a) assert a == pickle.loads(o) @cached_class class TestClass: def __init__(self, value: Any) -> None: self.value = value def test_caching(): # Test that instances are cached inst1 = TestClass(1) inst2 = TestClass(1) assert inst1 is inst2 inst3 = TestClass(2) assert inst1 is not inst3 def test_picklability(): # Test that instances can be pickled and unpickled original = TestClass(42) pickled = pickle.dumps(original) unpickled = pickle.loads(pickled) # Check that the unpickled instance has the same value assert original.value == unpickled.value # Check that the unpickled instance is the same as a newly created instance new_instance = TestClass(42) assert unpickled is new_instance def test_initialization(): init_count = 0 @cached_class class TestInitClass: def __init__(self): nonlocal init_count init_count += 1 inst1 = TestInitClass() inst2 = TestInitClass() assert init_count == 1 def test_class_identity(): # Ensure the decorated class is still recognized as the original class assert isinstance(TestClass(1), TestClass) assert type(TestClass(1)) is TestClass def test_multiple_arguments(): @cached_class class MultiArgClass: def __init__(self, a, b, c=3): self.args = (a, b, c) inst1 = MultiArgClass(1, 2) inst2 = MultiArgClass(1, 2) inst3 = MultiArgClass(1, 2, 4) assert inst1 is inst2 assert inst1 is not inst3 assert inst1.args == (1, 2, 3) assert inst3.args == (1, 2, 4) def test_different_argument_types(): @cached_class class MultiTypeClass: def __init__(self, a, b, c): self.a, self.b, self.c = a, b, c inst1 = MultiTypeClass(1, "string", (1, 2)) inst2 = MultiTypeClass(1, "string", (1, 2)) assert inst1 is inst2 inst3 = MultiTypeClass(1, "string", [1, 2]) # Unhashable argument assert inst1 is not inst3 def test_keyword_arguments(): @cached_class class KeywordClass: def __init__(self, a, b, c=3): self.a, self.b, self.c = a, b, c inst1 = KeywordClass(1, 2) inst2 = KeywordClass(a=1, b=2) assert inst1 is inst2 inst3 = KeywordClass(1, 2, 4) assert inst1 is not inst3 def test_inheritance_chain(): @cached_class class GrandParent: def __init__(self, value): self.value = value class Parent(GrandParent): pass @cached_class class Child(Parent): pass gp1 = GrandParent(1) gp2 = GrandParent(1) assert gp1 is gp2 p1 = Parent(1) p2 = Parent(1) assert p1 is p2 assert p1 is not gp1 c1 = Child(1) c2 = Child(1) assert c1 is c2 assert c1 is not p1 def test_memory_management(): @cached_class class WeakRefClass: def __init__(self, value): self.value = value inst = WeakRefClass(1) weak_ref = weakref.ref(inst) del inst gc.collect() assert weak_ref() is None # Creating a new instance should work new_inst = WeakRefClass(1) assert new_inst.value == 1 def test_exception_in_init(): init_count = 0 @cached_class class ExceptionClass: def __init__(self, value): nonlocal init_count init_count += 1 if init_count == 1: raise ValueError("First init failed") self.value = value with pytest.raises(ValueError): ExceptionClass(1) assert init_count == 1 # Second attempt should work and use cache inst1 = ExceptionClass(1) inst2 = ExceptionClass(1) assert inst1 is inst2 assert init_count == 2 def test_property_and_method_behavior(): @cached_class class PropertyClass: def __init__(self, value): self._value = value @property def value(self): return self._value def get_value(self): return self._value inst1 = PropertyClass(1) inst2 = PropertyClass(1) assert inst1 is inst2 assert inst1.value == 1 assert inst1.get_value() == 1 def test_class_method_and_static_method(): @cached_class class MethodClass: def __init__(self, value): self.value = value @classmethod def create(cls, value): return cls(value) @staticmethod def static_method(value): return value * 2 inst1 = MethodClass(1) inst2 = MethodClass.create(1) assert inst1 is inst2 assert MethodClass.static_method(5) == 10 def test_nested_cached_classes(): @cached_class class OuterClass: def __init__(self, value): self.value = value @cached_class class InnerClass: def __init__(self, inner_value): self.inner_value = inner_value outer1 = OuterClass(1) outer2 = OuterClass(1) assert outer1 is outer2 inner1 = outer1.InnerClass(2) inner2 = outer2.InnerClass(2) assert inner1 is inner2 def test_large_number_of_instances(): @cached_class class LargeNumberClass: def __init__(self, value): self.value = value instances = [LargeNumberClass(i) for i in range(1000)] for i, inst in enumerate(instances): assert inst is LargeNumberClass(i) monty-2024.7.29/tests/test_dev.py000066400000000000000000000201761465166210500165540ustar00rootroot00000000000000from __future__ import annotations import datetime import unittest import warnings from dataclasses import dataclass from unittest.mock import patch import pytest from monty.dev import deprecated, install_excepthook, requires # Set all warnings to always be triggered. warnings.simplefilter("always") class TestDecorator: def test_deprecated(self): def func_replace(): pass @deprecated(func_replace, "Use func_replace instead") def func_old(): """This is the old function.""" pass with warnings.catch_warnings(record=True) as w: # Trigger a warning. func_old() # Verify Warning and message assert issubclass(w[0].category, FutureWarning) assert "Use func_replace instead" in str(w[0].message) # Check metadata preservation assert func_old.__name__ == "func_old" assert func_old.__doc__ == "This is the old function." def test_deprecated_str_replacement(self): @deprecated("func_replace") def func_old(): pass with warnings.catch_warnings(record=True) as w: # Trigger a warning. func_old() # Verify Warning and message assert issubclass(w[0].category, FutureWarning) assert "use func_replace instead" in str(w[0].message) def test_deprecated_property(self): class TestClass: """A dummy class for tests.""" @property def property_a(self): pass @property @deprecated(property_a) def property_b(self): return "b" @deprecated(property_a) def func_a(self): return "a" with warnings.catch_warnings(record=True) as w: # Trigger a warning. assert TestClass().property_b == "b" # Verify warning type assert issubclass(w[-1].category, FutureWarning) with warnings.catch_warnings(record=True) as w: # Trigger a warning. assert TestClass().func_a() == "a" # Verify some things assert issubclass(w[-1].category, FutureWarning) def test_deprecated_classmethod(self): class TestClass: """A dummy class for tests.""" @classmethod def classmethod_a(cls): pass @classmethod @deprecated(classmethod_a) def classmethod_b(cls): return "b" with warnings.catch_warnings(record=True) as w: # Trigger a warning. assert TestClass().classmethod_b() == "b" # Verify some things assert issubclass(w[-1].category, FutureWarning) class TestClass_deprecationwarning: """A dummy class for tests.""" @classmethod def classmethod_a(cls): pass @classmethod @deprecated(classmethod_a, category=DeprecationWarning) def classmethod_b(cls): return "b" with pytest.warns(DeprecationWarning): assert TestClass_deprecationwarning().classmethod_b() == "b" def test_deprecated_class(self): class TestClassNew: """A dummy class for tests.""" def method_a(self): pass @deprecated(replacement=TestClassNew) class TestClassOld: """A dummy old class for tests.""" class_attrib_old = "OLD_ATTRIB" def method_b(self): """This is method_b.""" pass with pytest.warns(FutureWarning, match="TestClassOld is deprecated"): old_class = TestClassOld() # Check metadata preservation assert old_class.__doc__ == "A dummy old class for tests." assert old_class.class_attrib_old == "OLD_ATTRIB" assert old_class.__module__ == __name__ assert old_class.method_b.__doc__ == "This is method_b." def test_deprecated_dataclass(self): @dataclass class TestClassNew: """A dummy class for tests.""" def __post_init__(self): print("Hello.") def method_a(self): pass @deprecated(replacement=TestClassNew) @dataclass class TestClassOld: """A dummy old class for tests.""" class_attrib_old = "OLD_ATTRIB" def __post_init__(self): print("Hello.") def method_b(self): """This is method_b.""" pass with pytest.warns(FutureWarning, match="TestClassOld is deprecated"): old_class = TestClassOld() # Check metadata preservation assert old_class.__doc__ == "A dummy old class for tests." assert old_class.class_attrib_old == "OLD_ATTRIB" def test_deprecated_deadline(self, monkeypatch): with pytest.raises(DeprecationWarning): with patch("subprocess.run") as mock_run: monkeypatch.setenv("CI", "true") # mock CI env # Mock "GITHUB_REPOSITORY" monkeypatch.setenv("GITHUB_REPOSITORY", "TESTOWNER/TESTREPO") mock_run.return_value.stdout.decode.return_value = ( "git@github.com:TESTOWNER/TESTREPO.git" ) @deprecated(deadline=(2000, 1, 1)) def func_old(): pass @pytest.fixture() def test_deprecated_deadline_no_warn(self, monkeypatch): """Test cases where no warning should be raised.""" # No warn case 1: date before deadline with warnings.catch_warnings(): with patch("subprocess.run") as mock_run: monkeypatch.setenv("CI", "true") # mock CI env # Mock date to 1999-01-01 monkeypatch.setattr( datetime.datetime, "now", datetime.datetime(1999, 1, 1) ) # Mock "GITHUB_REPOSITORY" monkeypatch.setenv("GITHUB_REPOSITORY", "TESTOWNER/TESTREPO") mock_run.return_value.stdout.decode.return_value = ( "git@github.com:TESTOWNER/TESTREPO.git" ) @deprecated(deadline=(2000, 1, 1)) def func_old(): pass monkeypatch.undo() # No warn case 2: not in CI env with warnings.catch_warnings(): with patch("subprocess.run") as mock_run: monkeypatch.delenv("CI", raising=False) # Mock "GITHUB_REPOSITORY" monkeypatch.setenv("GITHUB_REPOSITORY", "TESTOWNER/TESTREPO") mock_run.return_value.stdout.decode.return_value = ( "git@github.com:TESTOWNER/TESTREPO.git" ) @deprecated(deadline=(2000, 1, 1)) def func_old_1(): pass monkeypatch.undo() # No warn case 3: not in code owner repo with warnings.catch_warnings(): monkeypatch.setenv("CI", "true") monkeypatch.delenv("GITHUB_REPOSITORY", raising=False) @deprecated(deadline=(2000, 1, 1)) def func_old_2(): pass def test_requires(self): try: import fictitious_mod except ImportError: fictitious_mod = None err_msg = "fictitious_mod is not present." @requires(fictitious_mod is not None, err_msg) def use_fictitious_mod(): print("success") with pytest.raises(RuntimeError, match=err_msg): use_fictitious_mod() @requires(unittest is not None, "unittest is not present.") def use_unittest(): return "success" assert use_unittest() == "success" # test with custom error class @requires(False, "expect ImportError", err_cls=ImportError) def use_import_error(): return "success" with pytest.raises(ImportError, match="expect ImportError"): use_import_error() def test_install_except_hook(self): install_excepthook() monty-2024.7.29/tests/test_files/000077500000000000000000000000001465166210500165205ustar00rootroot00000000000000monty-2024.7.29/tests/test_files/3000_lines.txt000066400000000000000000000331041465166210500210360ustar00rootroot000000000000001 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748 1749 1750 1751 1752 1753 1754 1755 1756 1757 1758 1759 1760 1761 1762 1763 1764 1765 1766 1767 1768 1769 1770 1771 1772 1773 1774 1775 1776 1777 1778 1779 1780 1781 1782 1783 1784 1785 1786 1787 1788 1789 1790 1791 1792 1793 1794 1795 1796 1797 1798 1799 1800 1801 1802 1803 1804 1805 1806 1807 1808 1809 1810 1811 1812 1813 1814 1815 1816 1817 1818 1819 1820 1821 1822 1823 1824 1825 1826 1827 1828 1829 1830 1831 1832 1833 1834 1835 1836 1837 1838 1839 1840 1841 1842 1843 1844 1845 1846 1847 1848 1849 1850 1851 1852 1853 1854 1855 1856 1857 1858 1859 1860 1861 1862 1863 1864 1865 1866 1867 1868 1869 1870 1871 1872 1873 1874 1875 1876 1877 1878 1879 1880 1881 1882 1883 1884 1885 1886 1887 1888 1889 1890 1891 1892 1893 1894 1895 1896 1897 1898 1899 1900 1901 1902 1903 1904 1905 1906 1907 1908 1909 1910 1911 1912 1913 1914 1915 1916 1917 1918 1919 1920 1921 1922 1923 1924 1925 1926 1927 1928 1929 1930 1931 1932 1933 1934 1935 1936 1937 1938 1939 1940 1941 1942 1943 1944 1945 1946 1947 1948 1949 1950 1951 1952 1953 1954 1955 1956 1957 1958 1959 1960 1961 1962 1963 1964 1965 1966 1967 1968 1969 1970 1971 1972 1973 1974 1975 1976 1977 1978 1979 1980 1981 1982 1983 1984 1985 1986 1987 1988 1989 1990 1991 1992 1993 1994 1995 1996 1997 1998 1999 2000 2001 2002 2003 2004 2005 2006 2007 2008 2009 2010 2011 2012 2013 2014 2015 2016 2017 2018 2019 2020 2021 2022 2023 2024 2025 2026 2027 2028 2029 2030 2031 2032 2033 2034 2035 2036 2037 2038 2039 2040 2041 2042 2043 2044 2045 2046 2047 2048 2049 2050 2051 2052 2053 2054 2055 2056 2057 2058 2059 2060 2061 2062 2063 2064 2065 2066 2067 2068 2069 2070 2071 2072 2073 2074 2075 2076 2077 2078 2079 2080 2081 2082 2083 2084 2085 2086 2087 2088 2089 2090 2091 2092 2093 2094 2095 2096 2097 2098 2099 2100 2101 2102 2103 2104 2105 2106 2107 2108 2109 2110 2111 2112 2113 2114 2115 2116 2117 2118 2119 2120 2121 2122 2123 2124 2125 2126 2127 2128 2129 2130 2131 2132 2133 2134 2135 2136 2137 2138 2139 2140 2141 2142 2143 2144 2145 2146 2147 2148 2149 2150 2151 2152 2153 2154 2155 2156 2157 2158 2159 2160 2161 2162 2163 2164 2165 2166 2167 2168 2169 2170 2171 2172 2173 2174 2175 2176 2177 2178 2179 2180 2181 2182 2183 2184 2185 2186 2187 2188 2189 2190 2191 2192 2193 2194 2195 2196 2197 2198 2199 2200 2201 2202 2203 2204 2205 2206 2207 2208 2209 2210 2211 2212 2213 2214 2215 2216 2217 2218 2219 2220 2221 2222 2223 2224 2225 2226 2227 2228 2229 2230 2231 2232 2233 2234 2235 2236 2237 2238 2239 2240 2241 2242 2243 2244 2245 2246 2247 2248 2249 2250 2251 2252 2253 2254 2255 2256 2257 2258 2259 2260 2261 2262 2263 2264 2265 2266 2267 2268 2269 2270 2271 2272 2273 2274 2275 2276 2277 2278 2279 2280 2281 2282 2283 2284 2285 2286 2287 2288 2289 2290 2291 2292 2293 2294 2295 2296 2297 2298 2299 2300 2301 2302 2303 2304 2305 2306 2307 2308 2309 2310 2311 2312 2313 2314 2315 2316 2317 2318 2319 2320 2321 2322 2323 2324 2325 2326 2327 2328 2329 2330 2331 2332 2333 2334 2335 2336 2337 2338 2339 2340 2341 2342 2343 2344 2345 2346 2347 2348 2349 2350 2351 2352 2353 2354 2355 2356 2357 2358 2359 2360 2361 2362 2363 2364 2365 2366 2367 2368 2369 2370 2371 2372 2373 2374 2375 2376 2377 2378 2379 2380 2381 2382 2383 2384 2385 2386 2387 2388 2389 2390 2391 2392 2393 2394 2395 2396 2397 2398 2399 2400 2401 2402 2403 2404 2405 2406 2407 2408 2409 2410 2411 2412 2413 2414 2415 2416 2417 2418 2419 2420 2421 2422 2423 2424 2425 2426 2427 2428 2429 2430 2431 2432 2433 2434 2435 2436 2437 2438 2439 2440 2441 2442 2443 2444 2445 2446 2447 2448 2449 2450 2451 2452 2453 2454 2455 2456 2457 2458 2459 2460 2461 2462 2463 2464 2465 2466 2467 2468 2469 2470 2471 2472 2473 2474 2475 2476 2477 2478 2479 2480 2481 2482 2483 2484 2485 2486 2487 2488 2489 2490 2491 2492 2493 2494 2495 2496 2497 2498 2499 2500 2501 2502 2503 2504 2505 2506 2507 2508 2509 2510 2511 2512 2513 2514 2515 2516 2517 2518 2519 2520 2521 2522 2523 2524 2525 2526 2527 2528 2529 2530 2531 2532 2533 2534 2535 2536 2537 2538 2539 2540 2541 2542 2543 2544 2545 2546 2547 2548 2549 2550 2551 2552 2553 2554 2555 2556 2557 2558 2559 2560 2561 2562 2563 2564 2565 2566 2567 2568 2569 2570 2571 2572 2573 2574 2575 2576 2577 2578 2579 2580 2581 2582 2583 2584 2585 2586 2587 2588 2589 2590 2591 2592 2593 2594 2595 2596 2597 2598 2599 2600 2601 2602 2603 2604 2605 2606 2607 2608 2609 2610 2611 2612 2613 2614 2615 2616 2617 2618 2619 2620 2621 2622 2623 2624 2625 2626 2627 2628 2629 2630 2631 2632 2633 2634 2635 2636 2637 2638 2639 2640 2641 2642 2643 2644 2645 2646 2647 2648 2649 2650 2651 2652 2653 2654 2655 2656 2657 2658 2659 2660 2661 2662 2663 2664 2665 2666 2667 2668 2669 2670 2671 2672 2673 2674 2675 2676 2677 2678 2679 2680 2681 2682 2683 2684 2685 2686 2687 2688 2689 2690 2691 2692 2693 2694 2695 2696 2697 2698 2699 2700 2701 2702 2703 2704 2705 2706 2707 2708 2709 2710 2711 2712 2713 2714 2715 2716 2717 2718 2719 2720 2721 2722 2723 2724 2725 2726 2727 2728 2729 2730 2731 2732 2733 2734 2735 2736 2737 2738 2739 2740 2741 2742 2743 2744 2745 2746 2747 2748 2749 2750 2751 2752 2753 2754 2755 2756 2757 2758 2759 2760 2761 2762 2763 2764 2765 2766 2767 2768 2769 2770 2771 2772 2773 2774 2775 2776 2777 2778 2779 2780 2781 2782 2783 2784 2785 2786 2787 2788 2789 2790 2791 2792 2793 2794 2795 2796 2797 2798 2799 2800 2801 2802 2803 2804 2805 2806 2807 2808 2809 2810 2811 2812 2813 2814 2815 2816 2817 2818 2819 2820 2821 2822 2823 2824 2825 2826 2827 2828 2829 2830 2831 2832 2833 2834 2835 2836 2837 2838 2839 2840 2841 2842 2843 2844 2845 2846 2847 2848 2849 2850 2851 2852 2853 2854 2855 2856 2857 2858 2859 2860 2861 2862 2863 2864 2865 2866 2867 2868 2869 2870 2871 2872 2873 2874 2875 2876 2877 2878 2879 2880 2881 2882 2883 2884 2885 2886 2887 2888 2889 2890 2891 2892 2893 2894 2895 2896 2897 2898 2899 2900 2901 2902 2903 2904 2905 2906 2907 2908 2909 2910 2911 2912 2913 2914 2915 2916 2917 2918 2919 2920 2921 2922 2923 2924 2925 2926 2927 2928 2929 2930 2931 2932 2933 2934 2935 2936 2937 2938 2939 2940 2941 2942 2943 2944 2945 2946 2947 2948 2949 2950 2951 2952 2953 2954 2955 2956 2957 2958 2959 2960 2961 2962 2963 2964 2965 2966 2967 2968 2969 2970 2971 2972 2973 2974 2975 2976 2977 2978 2979 2980 2981 2982 2983 2984 2985 2986 2987 2988 2989 2990 2991 2992 2993 2994 2995 2996 2997 2998 2999 3000monty-2024.7.29/tests/test_files/3000_lines.txt.bz2000066400000000000000000000067451465166210500215450ustar00rootroot00000000000000BZh91AY&SY %H@`j05RTUA@ hS'*fFT*￿}H <P@8c (t>B@Yn$ C|t  p $x(H >px $x(H <| $x(H :(b,:x@ , pd>: dc8 $x(H 8d>: dc$ C|t  r1H <P@8[t ށx(H )àb,:x@ , pb,:x@ , pb,:x@ , p뀡@P|1\@@<$ b,:x@ , p @@<$t @@<$8< @@<$p> @@<$p1H <P@8KY@P|1@@<$p@P|19p@P|1 @@<$$x(H 1*3%وO@s"""""""""Ȉ@1*2""""""""!< @ ̈O$"""""""""Ȉ@1*2""""""""!< @ ̈O$"""""""""! 0p@P|Y<b,:x@ ,Y (t>B@[y{{̂ + sx997mmA ֐kH5 iZA!>mcwwwwA ֐kH5 iZA!>m7{zkH5 iZA ֐kHom{zkH5 iZA ֐kHoݶcm{zkH5 iZA ֐kHom{oR iZA ֐kH5 i omwww~ޤ iZA ֐kH5lmԃZA ֐kH5 iZC|}m6m߷ZA ֐kH5 i.m p d>: d`d>:n7wwF C|t 8nwwwwzkH5 iZA ֐kHomlmۻR iZA ֐kH5 i nmmޤd*+ ssx97{kvm= + <盽m|m}o9<烜ssx99B@Y, C|cnd>2ffffff@m[m$[n6k-fI-7{m[mI$ۻmն4K`m}ɤ[vkmM$7{v_mmi$ݻm[mI$ ݶjlI%|1 @@<$b,:1@P: s333332vkmM$7{v_mmi$ݻm[mI$ ݶjlI%nmmm4[_mi$ݷͶm$Iowom~omI@v6lI%wvw|t  1H <P@8,p@t t :wweL|k{mM$7{dI-nmmm4[_mi$ݷͶm$Iowmm~[mmM$}_~[dI-7{vߖ$K~ ݵmI4ߠ&fwݐ:x@ , pd>: dc$ @ۻ>7{vߖ$K~ ݵmI4ߠwmm~[mmM$}_~[dI-7{vߖ$K~ ݵmI4ߠwmmlM%v{$K~ kmI433lY (t>B@Y C|t H>: dcww{vzffffff@ov~$Ioݶ{mI&߀wmmlM%v{$K~ kmI4m׶odi-7{ݯm߶$[nmI4wmmlI33}@@<$@b,:x@ , px(H 33332K333332vim$Kz7{MVm$[ݶjoI$ހ U}I&nmI4wmmlImmdM-m5[m$io@>mն}I'.p!<Jmonty-2024.7.29/tests/test_files/3000_lines.txt.gz000066400000000000000000000145401465166210500214600ustar00rootroot00000000000000=df3000_lines.txt%Ib=O p1enX븏x}|G#9‘u:ryG<:`|:c?r:}X:+q:zkwq9utsq<dz:}=}<Ǜx^{oǻ;ycc_Ǿo;/㻎>exqB+9{'g/쭜z΋:_zaz-טczMWe!zיgz鍦Wiz魦ךb=כM6rM7mڞ%^szE7^uzegz酧7^yz饧^{zya^}z姷^zi@ ۟k4C! HS-i|=hڃ=hڃ \{A{<=?=ϴ?Tcϵ?A{,{=hڃ=hڃ=7sA{A{?kڃ=hڃ=h\{A{A{ϳڃ=hڃ=hڃkڃ=hڃ=h>/cj=V{Xc_ ==V{Xcj \{Xcjof~n}/+=V{X.+\{Xcj=V{__==V{Xcj\ϵj=V{Xczscj=V{X9{=V{Xcj=Xl{\qW{\qW{\qϵW{\qW{\qDž{=j=j=k=jk`zo .ˠ=j=/k=j=jj=j=z j=j=U=W{\qW{\q痚jZk=n=n;~\{qw{qw{=sqw{qw{q/P{=n=n=oޞk=ٯgz{/in=n=k=n=nKn=n=>\{qw{qw{ܟ>GA=xi=xi=sO{<O{Gxm=xm=x#^ϵo{o{Njς=o{o{m=xm=x|xm=xm=xoC{=xm=xm=՞k=^h}ևZj}vl{Gxm=xm=pϵo{o{rcn=v{cLJk=v{cnO==v{cn{scn=v{/{=v{cn=ϵn=v{c~z=v{cn=(s56677889fcn=v{؟ÉIǓk=k=s_{|_{|s_{|_{|-k=k=krk=k=nGk=k=kq&k=k=^k=k=ko;Scsà3:D8#̄3T8c̅3:O8N|x: N#x:$'OItT<Oit\<5pd<Oөtl<Ott 000000׀?!1AQa                   !! "" ## $$ %% && '' (( )) ** ++ ,, -- .. // 00 11 22 3 4Dj!jC 8Drcg6W";Dw!C  k "BDH""FD="DH$"JD,]"DH&"ND<}"EH("RDL"BEH*"VD\"EH,"ZDl"EH."^D|"FH0"bDň#BFH2"fD͈=#FH4"jDՈ]#kFQ7"oD߈#G4Q9"sDБ588@?188 8"8$hs#Gd>|D#G@D$*Hd BD 1$jH!DD&Q$Hd"FD6q$H#HD"F$*Id$JD*V$jI%LD2f$Id&ND:v$8(QP"DC%2Jt)QR"DK5%rJ*QT"DSU%JqaK,X"De%BKH-Z"Dm%KH.\"Du%KH/^"D}%LH0`"D &BLH1b"D=&LH2d"D,]&LH3f"D<}&Mv"MT4i"DT&bMԚ5k"Dd&MT6m"Dt&Mԛ7o"D '"Ny 0=7W~b^ۀy`sA'N$h:uD։a'Nh;wDމ' O$h<yD'JOh={D'O$h>}D'Oh?D( P$h@E3lE 1(jPAE&Q(PdB E6q(PCE"F(*QdDE*V(jQEE23%м@h^y4/l.E?)R4HQ"#EG%)RRIQ"'EOE)R4JQ"+EWe)RKQ"/E_)S4LQ"3Eg)RS&rSNQ"9Es)StOQ"=E{)SPQ"AE*2TtQQ"EE5*rTRQ"IE(U*TtSQ"ME8u*TTy wpky'UŪUWEb*UDV[Er*UīW_E +VDXcEȊ)+ZVĬYgEʞ6״"jEՊ]+VH[Ѷ"nE݊}+WH\Ѹ"rE+BWH]Ѻ"vE+WH^Ѽ"zE+WH_Ѿ"~E+XH`"E ,BXHa"E =Wx^[y 'l.E8u,XdQ"E#H,2YteQ"E+X,rYfQ"E3h,l=\8r,YijgϢE@ -ZDhѢEH)-ZZĴiӢEPI-ZDjբEXi-ZĵkעE`-[Dl٢Eh-Z[ĶmۢEp-[DnݢEx-[ķ|^Y YY-0 0 00F9..ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppd@k ~bAfdVAfdAfdAlpb6Gu8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8fg~fgV~fg~fg~fgfgVfgfgfgfhV;@4@ 4@ 4@6u Š ՠ %5EUeušա !%"5#E$U%e&u'("5ی6X6+`_6{`6` fskvXmáááááááááááááq,wl"u8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8\=۟9:P?1K:k6xf_::::::::::::::::::::::::::::::::lO,\CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC㝿R;߳=߳=߳=p?1+6)6س?????aso":::::::::::::::::::::::::::::::::::::::::::::::::::::::::::??D6monty-2024.7.29/tests/test_files/3000lines.txt.gz000066400000000000000000000145371465166210500213270ustar00rootroot00000000000000#(2T3000lines.txt%Ib=O p1enX븏x}|G#9‘u:ryG<:`|:c?r:}X:+q:zkwq9utsq<dz:}=}<Ǜx^{oǻ;ycc_Ǿo;/㻎>exqB+9{'g/쭜z΋:_zaz-טczMWe!zיgz鍦Wiz魦ךb=כM6rM7mڞ%^szE7^uzegz酧7^yz饧^{zya^}z姷^zi@ ۟k4C! HS-i|=hڃ=hڃ \{A{<=?=ϴ?Tcϵ?A{,{=hڃ=hڃ=7sA{A{?kڃ=hڃ=h\{A{A{ϳڃ=hڃ=hڃkڃ=hڃ=h>/cj=V{Xc_ ==V{Xcj \{Xcjof~n}/+=V{X.+\{Xcj=V{__==V{Xcj\ϵj=V{Xczscj=V{X9{=V{Xcj=Xl{\qW{\qW{\qϵW{\qW{\qDž{=j=j=k=jk`zo .ˠ=j=/k=j=jj=j=z j=j=U=W{\qW{\q痚jZk=n=n;~\{qw{qw{=sqw{qw{q/P{=n=n=oޞk=ٯgz{/in=n=k=n=nKn=n=>\{qw{qw{ܟ>GA=xi=xi=sO{<O{Gxm=xm=x#^ϵo{o{Njς=o{o{m=xm=x|xm=xm=xoC{=xm=xm=՞k=^h}ևZj}vl{Gxm=xm=pϵo{o{rcn=v{cLJk=v{cnO==v{cn{scn=v{/{=v{cn=ϵn=v{c~z=v{cn=(s56677889fcn=v{؟ÉIǓk=k=s_{|_{|s_{|_{|-k=k=krk=k=nGk=k=kq&k=k=^k=k=ko;Scsà3:D8#̄3T8c̅3:O8N|x: N#x:$'OItT<Oit\<5pd<Oөtl<Ott 000000׀?!1AQa                   !! "" ## $$ %% && '' (( )) ** ++ ,, -- .. // 00 11 22 3 4Dj!jC 8Drcg6W";Dw!C  k "BDH""FD="DH$"JD,]"DH&"ND<}"EH("RDL"BEH*"VD\"EH,"ZDl"EH."^D|"FH0"bDň#BFH2"fD͈=#FH4"jDՈ]#kFQ7"oD߈#G4Q9"sDБ588@?188 8"8$hs#Gd>|D#G@D$*Hd BD 1$jH!DD&Q$Hd"FD6q$H#HD"F$*Id$JD*V$jI%LD2f$Id&ND:v$8(QP"DC%2Jt)QR"DK5%rJ*QT"DSU%JqaK,X"De%BKH-Z"Dm%KH.\"Du%KH/^"D}%LH0`"D &BLH1b"D=&LH2d"D,]&LH3f"D<}&Mv"MT4i"DT&bMԚ5k"Dd&MT6m"Dt&Mԛ7o"D '"Ny 0=7W~b^ۀy`sA'N$h:uD։a'Nh;wDމ' O$h<yD'JOh={D'O$h>}D'Oh?D( P$h@E3lE 1(jPAE&Q(PdB E6q(PCE"F(*QdDE*V(jQEE23%м@h^y4/l.E?)R4HQ"#EG%)RRIQ"'EOE)R4JQ"+EWe)RKQ"/E_)S4LQ"3Eg)RS&rSNQ"9Es)StOQ"=E{)SPQ"AE*2TtQQ"EE5*rTRQ"IE(U*TtSQ"ME8u*TTy wpky'UŪUWEb*UDV[Er*UīW_E +VDXcEȊ)+ZVĬYgEʞ6״"jEՊ]+VH[Ѷ"nE݊}+WH\Ѹ"rE+BWH]Ѻ"vE+WH^Ѽ"zE+WH_Ѿ"~E+XH`"E ,BXHa"E =Wx^[y 'l.E8u,XdQ"E#H,2YteQ"E+X,rYfQ"E3h,l=\8r,YijgϢE@ -ZDhѢEH)-ZZĴiӢEPI-ZDjբEXi-ZĵkעE`-[Dl٢Eh-Z[ĶmۢEp-[DnݢEx-[ķ|^Y YY-0 0 00F9..ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppd@k ~bAfdVAfdAfdAlpb6Gu8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8fg~fgV~fg~fg~fgfgVfgfgfgfhV;@4@ 4@ 4@6u Š ՠ %5EUeušա !%"5#E$U%e&u'("5ی6X6+`_6{`6` fskvXmáááááááááááááq,wl"u8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8t8\=۟9:P?1K:k6xf_::::::::::::::::::::::::::::::::lO,\CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC㝿R;߳=߳=߳=p?1+6)6س?????aso":::::::::::::::::::::::::::::::::::::::::::::::::::::::::::??D6monty-2024.7.29/tests/test_files/empty_file.txt000066400000000000000000000000001465166210500214040ustar00rootroot00000000000000monty-2024.7.29/tests/test_files/myfile000066400000000000000000000000151465166210500177240ustar00rootroot00000000000000HelloWorld. monty-2024.7.29/tests/test_files/myfile_bz2.bz2000066400000000000000000000000701465166210500211760ustar00rootroot00000000000000BZh91AY&SYc׀@ 1LA 2CjM]BBmonty-2024.7.29/tests/test_files/myfile_gz.gz000066400000000000000000000000501465166210500210420ustar00rootroot00000000000000-TmyfileH/IWK\ monty-2024.7.29/tests/test_files/myfile_lzma.lzma000066400000000000000000000000451465166210500217140ustar00rootroot00000000000000]$Io+o#,-sdmonty-2024.7.29/tests/test_files/myfile_txt000066400000000000000000000000151465166210500206230ustar00rootroot00000000000000HelloWorld. monty-2024.7.29/tests/test_files/myfile_xz.xz000066400000000000000000000001101465166210500211010ustar00rootroot000000000000007zXZִF!t/ HelloWorld. f0ЧjVS% qĶ}YZmonty-2024.7.29/tests/test_files/test_settings.yaml000066400000000000000000000000541465166210500223020ustar00rootroot00000000000000old_module.old_class : new_module.new_class monty-2024.7.29/tests/test_fnmatch.py000066400000000000000000000002641465166210500174120ustar00rootroot00000000000000from __future__ import annotations from monty.fnmatch import WildCard def test_match(): wc = WildCard("*.pdf") assert wc.match("A.pdf") assert not wc.match("A.pdg") monty-2024.7.29/tests/test_fractions.py000066400000000000000000000004621465166210500177620ustar00rootroot00000000000000from __future__ import annotations import pytest from monty.fractions import gcd, gcd_float, lcm def test_gcd(): assert gcd(7, 14, 63) == 7 def test_lcm(): assert lcm(2, 3, 4) == 12 def test_gcd_float(): vs = [6.2, 12.4, 15.5 + 5e-9] assert gcd_float(vs, 1e-8) == pytest.approx(3.1) monty-2024.7.29/tests/test_functools.py000066400000000000000000000404061465166210500200100ustar00rootroot00000000000000from __future__ import annotations import platform import time import unittest import pytest from monty.functools import ( TimeoutError, lazy_property, prof_main, return_if_raise, return_none_if_raise, timeout, ) class TestLazy: def test_evaluate(self): # Lazy attributes should be evaluated when accessed. called = [] class Foo: @lazy_property def foo(self): called.append("foo") return 1 f = Foo() assert f.foo == 1 assert len(called) == 1 def test_evaluate_once(self): # lazy_property attributes should be evaluated only once. called = [] class Foo: @lazy_property def foo(self): called.append("foo") return 1 f = Foo() assert f.foo == 1 assert f.foo == 1 assert f.foo == 1 assert len(called) == 1 def test_private_attribute(self): # It should be possible to create private, name-mangled # lazy_property attributes. called = [] class Foo: @lazy_property def __foo(self): called.append("foo") return 1 def get_foo(self): return self.__foo f = Foo() assert f.get_foo() == 1 assert f.get_foo() == 1 assert f.get_foo() == 1 assert len(called) == 1 def test_reserved_attribute(self): # It should be possible to create reserved lazy_property attributes. called = [] class Foo: @lazy_property def __foo__(self): called.append("foo") return 1 f = Foo() assert f.__foo__ == 1 assert f.__foo__ == 1 assert f.__foo__ == 1 assert len(called) == 1 def test_result_shadows_descriptor(self): # The result of the function call should be stored in # the object __dict__, shadowing the descriptor. called = [] class Foo: @lazy_property def foo(self): called.append("foo") return 1 f = Foo() assert isinstance(Foo.foo, lazy_property) assert f.foo is f.foo assert f.foo is f.__dict__["foo"] # ! assert len(called) == 1 assert f.foo == 1 assert f.foo == 1 assert len(called) == 1 lazy_property.invalidate(f, "foo") assert f.foo == 1 assert len(called) == 2 assert f.foo == 1 assert f.foo == 1 assert len(called) == 2 def test_readonly_object(self): # The descriptor should raise an AttributeError when lazy_property is # used on a read-only object (an object with __slots__). called = [] class Foo: __slots__ = () @lazy_property def foo(self): called.append("foo") return 1 f = Foo() assert len(called) == 0 with pytest.raises( AttributeError, match="'Foo' object has no attribute '__dict__'" ): f.foo # The function was not called assert len(called) == 0 def test_introspection(self): # The lazy_property decorator should support basic introspection. class Foo: def foo(self): """foo func doc""" @lazy_property def bar(self): """bar func doc""" assert Foo.foo.__name__ == "foo" assert Foo.foo.__doc__ == "foo func doc" assert "test_functools" in Foo.foo.__module__ assert Foo.bar.__name__ == "bar" assert Foo.bar.__doc__ == "bar func doc" assert "test_functools" in Foo.bar.__module__ class TestInvalidate: def test_invalidate_attribute(self): # It should be possible to invalidate a lazy_property attribute. called = [] class Foo: @lazy_property def foo(self): called.append("foo") return 1 f = Foo() assert f.foo == 1 assert len(called) == 1 lazy_property.invalidate(f, "foo") assert f.foo == 1 assert len(called) == 2 def test_invalidate_attribute_twice(self): # It should be possible to invalidate a lazy_property attribute # twice without causing harm. called = [] class Foo: @lazy_property def foo(self): called.append("foo") return 1 f = Foo() assert f.foo == 1 assert len(called) == 1 lazy_property.invalidate(f, "foo") lazy_property.invalidate(f, "foo") # Nothing happens assert f.foo == 1 assert len(called) == 2 def test_invalidate_uncalled_attribute(self): # It should be possible to invalidate an empty attribute # cache without causing harm. called = [] class Foo: @lazy_property def foo(self): called.append("foo") return 1 f = Foo() assert len(called) == 0 lazy_property.invalidate(f, "foo") # Nothing happens def test_invalidate_private_attribute(self): # It should be possible to invalidate a private lazy_property attribute. called = [] class Foo: @lazy_property def __foo(self): called.append("foo") return 1 def get_foo(self): return self.__foo f = Foo() assert f.get_foo() == 1 assert len(called) == 1 lazy_property.invalidate(f, "__foo") assert f.get_foo() == 1 assert len(called) == 2 def test_invalidate_mangled_attribute(self): # It should be possible to invalidate a private lazy_property attribute # by its mangled name. called = [] class Foo: @lazy_property def __foo(self): called.append("foo") return 1 def get_foo(self): return self.__foo f = Foo() assert f.get_foo() == 1 assert len(called) == 1 lazy_property.invalidate(f, "_Foo__foo") assert f.get_foo() == 1 assert len(called) == 2 def test_invalidate_reserved_attribute(self): # It should be possible to invalidate a reserved lazy_property attribute. called = [] class Foo: @lazy_property def __foo__(self): called.append("foo") return 1 f = Foo() assert f.__foo__ == 1 assert len(called) == 1 lazy_property.invalidate(f, "__foo__") assert f.__foo__ == 1 assert len(called) == 2 def test_invalidate_nonlazy_attribute(self): # Invalidating an attribute that is not lazy_property should # raise an AttributeError. called = [] class Foo: def foo(self): called.append("foo") return 1 f = Foo() with pytest.raises( AttributeError, match="'Foo.foo' is not a lazy_property attribute" ): lazy_property.invalidate(f, "foo") def test_invalidate_nonlazy_private_attribute(self): # Invalidating a private attribute that is not lazy_property should # raise an AttributeError. called = [] class Foo: def __foo(self): called.append("foo") return 1 f = Foo() with pytest.raises( AttributeError, match="type object 'Foo' has no attribute 'foo'" ): lazy_property.invalidate(f, "foo") def test_invalidate_unknown_attribute(self): # Invalidating an unknown attribute should # raise an AttributeError. called = [] class Foo: @lazy_property def foo(self): called.append("foo") return 1 f = Foo() with pytest.raises( AttributeError, match="type object 'Foo' has no attribute 'bar'" ): lazy_property.invalidate(f, "bar") def test_invalidate_readonly_object(self): # Calling invalidate on a read-only object should # raise an AttributeError. called = [] class Foo: __slots__ = () @lazy_property def foo(self): called.append("foo") return 1 f = Foo() with pytest.raises( AttributeError, match="'Foo' object has no attribute '__dict__'" ): lazy_property.invalidate(f, "foo") # A lazy_property subclass class cached(lazy_property): pass class TestInvalidateSubclass: def test_invalidate_attribute(self): # It should be possible to invalidate a cached attribute. called = [] class Bar: @cached def bar(self): called.append("bar") return 1 b = Bar() assert b.bar == 1 assert len(called) == 1 cached.invalidate(b, "bar") assert b.bar == 1 assert len(called) == 2 def test_invalidate_attribute_twice(self): # It should be possible to invalidate a cached attribute # twice without causing harm. called = [] class Bar: @cached def bar(self): called.append("bar") return 1 b = Bar() assert b.bar == 1 assert len(called) == 1 cached.invalidate(b, "bar") cached.invalidate(b, "bar") # Nothing happens assert b.bar == 1 assert len(called) == 2 def test_invalidate_uncalled_attribute(self): # It should be possible to invalidate an empty attribute # cache without causing harm. called = [] class Bar: @cached def bar(self): called.append("bar") return 1 b = Bar() assert len(called) == 0 cached.invalidate(b, "bar") # Nothing happens def test_invalidate_private_attribute(self): # It should be possible to invalidate a private cached attribute. called = [] class Bar: @cached def __bar(self): called.append("bar") return 1 def get_bar(self): return self.__bar b = Bar() assert b.get_bar() == 1 assert len(called) == 1 cached.invalidate(b, "__bar") assert b.get_bar() == 1 assert len(called) == 2 def test_invalidate_mangled_attribute(self): # It should be possible to invalidate a private cached attribute # by its mangled name. called = [] class Bar: @cached def __bar(self): called.append("bar") return 1 def get_bar(self): return self.__bar b = Bar() assert b.get_bar() == 1 assert len(called) == 1 cached.invalidate(b, "_Bar__bar") assert b.get_bar() == 1 assert len(called) == 2 def test_invalidate_reserved_attribute(self): # It should be possible to invalidate a reserved cached attribute. called = [] class Bar: @cached def __bar__(self): called.append("bar") return 1 b = Bar() assert b.__bar__ == 1 assert len(called) == 1 cached.invalidate(b, "__bar__") assert b.__bar__ == 1 assert len(called) == 2 def test_invalidate_uncached_attribute(self): # Invalidating an attribute that is not cached should # raise an AttributeError. called = [] class Bar: def bar(self): called.append("bar") return 1 b = Bar() with pytest.raises(AttributeError, match="'Bar.bar' is not a cached attribute"): cached.invalidate(b, "bar") def test_invalidate_uncached_private_attribute(self): # Invalidating a private attribute that is not cached should # raise an AttributeError. called = [] class Bar: def __bar(self): called.append("bar") return 1 b = Bar() with pytest.raises( AttributeError, match="'Bar._Bar__bar' is not a cached attribute" ): cached.invalidate( b, "__bar", ) def test_invalidate_unknown_attribute(self): # Invalidating an unknown attribute should # raise an AttributeError. called = [] class Bar: @cached def bar(self): called.append("bar") return 1 b = Bar() with pytest.raises( AttributeError, match="type object 'Bar' has no attribute 'baz'" ): lazy_property.invalidate( b, "baz", ) def test_invalidate_readonly_object(self): # Calling invalidate on a read-only object should # raise an AttributeError. called = [] class Bar: __slots__ = () @cached def bar(self): called.append("bar") return 1 b = Bar() with pytest.raises( AttributeError, match="'Bar' object has no attribute '__dict__'" ): cached.invalidate( b, "bar", ) def test_invalidate_superclass_attribute(self): # cached.invalidate CANNOT invalidate a superclass (lazy_property) attribute. called = [] class Bar: @lazy_property def bar(self): called.append("bar") return 1 b = Bar() with pytest.raises(AttributeError, match="'Bar.bar' is not a cached attribute"): cached.invalidate( b, "bar", ) def test_invalidate_subclass_attribute(self): # Whereas lazy_property.invalidate CAN invalidate a subclass (cached) attribute. called = [] class Bar: @cached def bar(self): called.append("bar") return 1 b = Bar() assert b.bar == 1 assert len(called) == 1 lazy_property.invalidate(b, "bar") assert b.bar == 1 assert len(called) == 2 class TestTryOrReturn: def test_decorator(self): class A: @return_if_raise(ValueError, "hello") def return_one(self): return 1 @return_if_raise(ValueError, "hello") def return_hello(self): raise ValueError @return_if_raise(KeyError, "hello") def reraise_value_error(self): raise ValueError @return_if_raise([KeyError, ValueError], "hello") def catch_exc_list(self): import random if random.randint(0, 1) == 0: raise ValueError raise KeyError @return_none_if_raise(TypeError) def return_none(self): raise TypeError a = A() assert a.return_one() == 1 assert a.return_hello() == "hello" with pytest.raises(ValueError): a.reraise_value_error() assert a.catch_exc_list() == "hello" assert a.return_none() is None class TestTimeout: @unittest.skipIf(platform.system() == "Windows", "Skip on windows") def test_with(self): try: with timeout(1, "timeout!"): time.sleep(2) raise AssertionError("Did not timeout!") except TimeoutError as ex: assert ex.message == "timeout!" class TestProfMain: def test_prof_decorator(self): """Testing prof_main decorator.""" import sys @prof_main def main(): return sys.exit(1) # Have to change argv before calling main. # Will restore original values before returning. _ = sys.argv[:] if len(sys.argv) == 1: sys.argv.append("prof") else: sys.argv[1] = "prof" monty-2024.7.29/tests/test_inspect.py000066400000000000000000000007771465166210500174500ustar00rootroot00000000000000from __future__ import annotations from monty.inspect import all_subclasses, caller_name, find_top_pyfile class LittleCatA: pass class LittleCatB(LittleCatA): pass class LittleCatC: pass class LittleCatD(LittleCatB): pass class TestInspect: def test_func(self): # Not a real test. Need something better. assert find_top_pyfile() assert caller_name() def test_all_subclasses(self): assert all_subclasses(LittleCatA) == [LittleCatB, LittleCatD] monty-2024.7.29/tests/test_io.py000066400000000000000000000164041465166210500164040ustar00rootroot00000000000000from __future__ import annotations import os from pathlib import Path from unittest.mock import patch import pytest from monty.io import ( FileLock, FileLockException, reverse_readfile, reverse_readline, zopen, ) from monty.tempfile import ScratchDir TEST_DIR = os.path.join(os.path.dirname(__file__), "test_files") class TestReverseReadline: NUMLINES = 3000 def test_reverse_readline(self): """ We are making sure a file containing line numbers is read in reverse order, i.e. the first line that is read corresponds to the last line. number """ with open(os.path.join(TEST_DIR, "3000_lines.txt")) as f: for idx, line in enumerate(reverse_readline(f)): assert ( int(line) == self.NUMLINES - idx ), f"read_backwards read {line} whereas it should have read {self.NUMLINES - idx}" def test_reverse_readline_fake_big(self): """ Make sure that large text files are read properly. """ with open(os.path.join(TEST_DIR, "3000_lines.txt")) as f: for idx, line in enumerate(reverse_readline(f, max_mem=0)): assert ( int(line) == self.NUMLINES - idx ), f"read_backwards read {line} whereas it should have read {self.NUMLINES - idx}" def test_reverse_readline_bz2(self): """ Make sure a file containing line numbers is read in reverse order, i.e. the first line that is read corresponds to the last line number. """ lines = [] with zopen(os.path.join(TEST_DIR, "myfile_bz2.bz2"), "rb") as f: for line in reverse_readline(f): lines.append(line.strip()) assert lines[-1].strip(), ["HelloWorld." in b"HelloWorld."] def test_empty_file(self): """ Make sure an empty file does not throw an error when reverse_readline is called, which was a problem with an earlier implementation. """ with open(os.path.join(TEST_DIR, "empty_file.txt")) as f: for _line in reverse_readline(f): raise ValueError("an empty file is being read!") @pytest.fixture() def test_line_ending(self): contents = ("Line1", "Line2", "Line3") # Mock Linux/MacOS with patch("os.name", "posix"): linux_line_end = os.linesep assert linux_line_end == "\n" with ScratchDir("./test_files"): with open("sample_unix_mac.txt", "w", newline=linux_line_end) as file: file.write(linux_line_end.join(contents)) with open("sample_unix_mac.txt") as file: for idx, line in enumerate(reverse_readfile(file)): assert line == contents[len(contents) - idx - 1] # Mock Windows with patch("os.name", "nt"): windows_line_end = os.linesep assert windows_line_end == "\r\n" with ScratchDir("./test_files"): with open("sample_windows.txt", "w", newline=windows_line_end) as file: file.write(windows_line_end.join(contents)) with open("sample_windows.txt") as file: for idx, line in enumerate(reverse_readfile(file)): assert line == contents[len(contents) - idx - 1] class TestReverseReadfile: NUMLINES = 3000 def test_reverse_readfile(self): """ Make sure a file containing line numbers is read in reverse order, i.e. the first line that is read corresponds to the last line number. """ fname = os.path.join(TEST_DIR, "3000_lines.txt") for idx, line in enumerate(reverse_readfile(fname)): assert int(line) == self.NUMLINES - idx def test_reverse_readfile_gz(self): """ Make sure a file containing line numbers is read in reverse order, i.e. the first line that is read corresponds to the last line number. """ fname = os.path.join(TEST_DIR, "3000_lines.txt.gz") for idx, line in enumerate(reverse_readfile(fname)): assert int(line) == self.NUMLINES - idx def test_reverse_readfile_bz2(self): """ Make sure a file containing line numbers is read in reverse order, i.e. the first line that is read corresponds to the last line number. """ fname = os.path.join(TEST_DIR, "3000_lines.txt.bz2") for idx, line in enumerate(reverse_readfile(fname)): assert int(line) == self.NUMLINES - idx def test_empty_file(self): """ Make sure an empty file does not throw an error when reverse_readline is called, which was a problem with an earlier implementation. """ for _line in reverse_readfile(os.path.join(TEST_DIR, "empty_file.txt")): raise ValueError("an empty file is being read!") @pytest.fixture def test_line_ending(self): contents = ("Line1", "Line2", "Line3") # Mock Linux/MacOS with patch("os.name", "posix"): linux_line_end = os.linesep assert linux_line_end == "\n" with ScratchDir("./test_files"): with open("sample_unix_mac.txt", "w", newline=linux_line_end) as file: file.write(linux_line_end.join(contents)) for idx, line in enumerate(reverse_readfile("sample_unix_mac.txt")): assert line == contents[len(contents) - idx - 1] # Mock Windows with patch("os.name", "nt"): windows_line_end = os.linesep assert windows_line_end == "\r\n" with ScratchDir("./test_files"): with open("sample_windows.txt", "w", newline=windows_line_end) as file: file.write(windows_line_end.join(contents)) for idx, line in enumerate(reverse_readfile("sample_windows.txt")): assert line == contents[len(contents) - idx - 1] class TestZopen: def test_zopen(self): with zopen(os.path.join(TEST_DIR, "myfile_gz.gz"), mode="rt") as f: assert f.read() == "HelloWorld.\n\n" with zopen(os.path.join(TEST_DIR, "myfile_bz2.bz2"), mode="rt") as f: assert f.read() == "HelloWorld.\n\n" with zopen(os.path.join(TEST_DIR, "myfile_bz2.bz2"), "rt") as f: assert f.read() == "HelloWorld.\n\n" with zopen(os.path.join(TEST_DIR, "myfile_xz.xz"), "rt") as f: assert f.read() == "HelloWorld.\n\n" with zopen(os.path.join(TEST_DIR, "myfile_lzma.lzma"), "rt") as f: assert f.read() == "HelloWorld.\n\n" with zopen(os.path.join(TEST_DIR, "myfile"), mode="rt") as f: assert f.read() == "HelloWorld.\n\n" def test_Path_objects(self): p = Path(TEST_DIR) / "myfile_gz.gz" with zopen(p, mode="rt") as f: assert f.read() == "HelloWorld.\n\n" class TestFileLock: def setup_method(self): self.file_name = "__lock__" self.lock = FileLock(self.file_name, timeout=1) self.lock.acquire() def test_raise(self): with pytest.raises(FileLockException): new_lock = FileLock(self.file_name, timeout=1) new_lock.acquire() def teardown_method(self): self.lock.release() monty-2024.7.29/tests/test_itertools.py000066400000000000000000000007301465166210500200140ustar00rootroot00000000000000# Copyright (c) Materials Virtual Lab. # Distributed under the terms of the BSD License. from __future__ import annotations from monty.itertools import chunks, iterator_from_slice def test_iterator_from_slice(): assert list(iterator_from_slice(slice(0, 6, 2))) == [0, 2, 4] def test_chunks(): assert list(chunks(range(1, 25), 10)) == [ (1, 2, 3, 4, 5, 6, 7, 8, 9, 10), (11, 12, 13, 14, 15, 16, 17, 18, 19, 20), (21, 22, 23, 24), ] monty-2024.7.29/tests/test_json.py000066400000000000000000001050721465166210500167460ustar00rootroot00000000000000from __future__ import annotations import dataclasses import datetime import json import os import pathlib from enum import Enum from typing import Union try: import numpy as np except ImportError: np = None try: import pandas as pd except ImportError: pd = None try: import pint except ImportError: pint = None try: import torch except ImportError: torch = None try: import pydantic except ImportError: pydantic = None try: from bson.objectid import ObjectId except ImportError: ObjectId = None import pytest from monty.json import ( MontyDecoder, MontyEncoder, MSONable, _load_redirect, jsanitize, load, ) from . import __version__ as tests_version TEST_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "test_files") class GoodMSONClass(MSONable): def __init__(self, a, b, c, d=1, *values, **kwargs): self.a = a self.b = b self._c = c self._d = d self.values = values self.kwargs = kwargs def __eq__(self, other): return ( self.a == other.a and self.b == other.b and self._c == other._c and self._d == other._d and self.kwargs == other.kwargs and self.values == other.values ) class GoodNOTMSONClass: """Literally the same as the GoodMSONClass, except it does not have the MSONable inheritance!""" def __init__(self, a, b, c, d=1, *values, **kwargs): self.a = a self.b = b self._c = c self._d = d self.values = values self.kwargs = kwargs def __eq__(self, other): return ( self.a == other.a and self.b == other.b and self._c == other._c and self._d == other._d and self.kwargs == other.kwargs and self.values == other.values ) class LimitedMSONClass(MSONable): """An MSONable class that only accepts a limited number of options""" def __init__(self, a): self.a = a def __eq__(self, other): return self.a == other.a class GoodNestedMSONClass(MSONable): def __init__(self, a_list, b_dict, c_list_dict_list, **kwargs): assert isinstance(a_list, list) assert isinstance(b_dict, dict) assert isinstance(c_list_dict_list, list) assert isinstance(c_list_dict_list[0], dict) first_key = next(iter(c_list_dict_list[0])) assert isinstance(c_list_dict_list[0][first_key], list) self.a_list = a_list self.b_dict = b_dict self._c_list_dict_list = c_list_dict_list self.kwargs = kwargs class MethodSerializationClass(MSONable): def __init__(self, a): self.a = a def method(self): pass @staticmethod def staticmethod(self): pass @classmethod def classmethod(cls): pass def __call__(self, b): # override call for instances return self.__class__(b) class NestedClass: def inner_method(self): pass class MethodNonSerializationClass: def __init__(self, a): self.a = a def method(self): pass def my_callable(a, b): return a + b class EnumNoAsDict(Enum): name_a = "value_a" name_b = "value_b" class EnumAsDict(Enum): name_a = "value_a" name_b = "value_b" def as_dict(self): return {"v": self.value} @classmethod def from_dict(cls, d): return cls(d["v"]) class EnumTest(MSONable, Enum): a = 1 b = 2 class ClassContainingDataFrame(MSONable): def __init__(self, df): self.df = df class ClassContainingSeries(MSONable): def __init__(self, s): self.s = s class ClassContainingQuantity(MSONable): def __init__(self, qty): self.qty = qty class ClassContainingNumpyArray(MSONable): def __init__(self, np_a): self.np_a = np_a @dataclasses.dataclass class Point: x: float = 1 y: float = 2 class Coordinates(MSONable): def __init__(self, points): self.points = points def __str__(self): return str(self.points) @dataclasses.dataclass class NestedDataClass: points: list[Point] class TestMSONable: def setup_method(self): self.good_cls = GoodMSONClass class BadMSONClass(MSONable): def __init__(self, a, b): self.a = a self.b = b def as_dict(self): return {"init": {"a": self.a, "b": self.b}} self.bad_cls = BadMSONClass class BadMSONClass2(MSONable): def __init__(self, a, b): self.a = a self.c = b self.bad_cls2 = BadMSONClass2 class AutoMSON(MSONable): def __init__(self, a, b): self.a = a self.b = b self.auto_mson = AutoMSON class ClassContainingKWOnlyArgs(MSONable): def __init__(self, *, a): self.a = a self.kw_only_args_cls = ClassContainingKWOnlyArgs def test_to_from_dict(self): obj = self.good_cls("Hello", "World", "Python") d = obj.as_dict() assert d is not None self.good_cls.from_dict(d) jsonstr = obj.to_json() d = json.loads(jsonstr) assert d["@class"], "GoodMSONClass" obj = self.bad_cls("Hello", "World") d = obj.as_dict() assert d is not None with pytest.raises(TypeError): self.bad_cls.from_dict(d) obj = self.bad_cls2("Hello", "World") with pytest.raises(NotImplementedError): obj.as_dict() obj = self.auto_mson(2, 3) d = obj.as_dict() self.auto_mson.from_dict(d) def test_kw_only_args(self): obj = self.kw_only_args_cls(a=1) d = obj.as_dict() assert d is not None assert d["a"] == 1 self.kw_only_args_cls.from_dict(d) jsonstr = obj.to_json() d = json.loads(jsonstr) assert d["@class"], "ClassContainingKWOnlyArgs" def test_unsafe_hash(self): GMC = GoodMSONClass a_list = [GMC(1, 1.0, "one"), GMC(2, 2.0, "two")] b_dict = {"first": GMC(3, 3.0, "three"), "second": GMC(4, 4.0, "four")} c_list_dict_list = [ { "list1": [ GMC(5, 5.0, "five"), GMC(6, 6.0, "six"), GMC(7, 7.0, "seven"), ], "list2": [GMC(8, 8.0, "eight")], }, { "list3": [ GMC(9, 9.0, "nine"), GMC(10, 10.0, "ten"), GMC(11, 11.0, "eleven"), GMC(12, 12.0, "twelve"), ], "list4": [GMC(13, 13.0, "thirteen"), GMC(14, 14.0, "fourteen")], "list5": [GMC(15, 15.0, "fifteen")], }, ] obj = GoodNestedMSONClass( a_list=a_list, b_dict=b_dict, c_list_dict_list=c_list_dict_list ) assert ( a_list[0].unsafe_hash().hexdigest() == "ea44de0e2ef627be582282c02c48e94de0d58ec6" ) assert ( obj.unsafe_hash().hexdigest() == "44204c8da394e878f7562c9aa2e37c2177f28b81" ) def test_version(self): obj = self.good_cls("Hello", "World", "Python") d = obj.as_dict() assert d["@version"] == tests_version def test_nested_to_from_dict(self): GMC = GoodMSONClass a_list = [GMC(1, 1.0, "one"), GMC(2, 2.0, "two")] b_dict = {"first": GMC(3, 3.0, "three"), "second": GMC(4, 4.0, "four")} c_list_dict_list = [ { "list1": [ GMC(5, 5.0, "five"), GMC(6, 6.0, "six"), GMC(7, 7.0, "seven"), ], "list2": [GMC(8, 8.0, "eight")], }, { "list3": [ GMC(9, 9.0, "nine"), GMC(10, 10.0, "ten"), GMC(11, 11.0, "eleven"), GMC(12, 12.0, "twelve"), ], "list4": [GMC(13, 13.0, "thirteen"), GMC(14, 14.0, "fourteen")], "list5": [GMC(15, 15.0, "fifteen")], }, ] obj = GoodNestedMSONClass( a_list=a_list, b_dict=b_dict, c_list_dict_list=c_list_dict_list ) obj_dict = obj.as_dict() obj2 = GoodNestedMSONClass.from_dict(obj_dict) assert [obj2.a_list[ii] == aa for ii, aa in enumerate(obj.a_list)] assert [obj2.b_dict[kk] == val for kk, val in obj.b_dict.items()] assert len(obj.a_list) == len(obj2.a_list) assert len(obj.b_dict) == len(obj2.b_dict) s = json.dumps(obj_dict) obj3 = json.loads(s, cls=MontyDecoder) assert [obj2.a_list[ii] == aa for ii, aa in enumerate(obj3.a_list)] assert [obj2.b_dict[kk] == val for kk, val in obj3.b_dict.items()] assert len(obj3.a_list) == len(obj2.a_list) assert len(obj3.b_dict) == len(obj2.b_dict) s = json.dumps(obj, cls=MontyEncoder) obj4 = json.loads(s, cls=MontyDecoder) assert [obj4.a_list[ii] == aa for ii, aa in enumerate(obj.a_list)] assert [obj4.b_dict[kk] == val for kk, val in obj.b_dict.items()] assert len(obj.a_list) == len(obj4.a_list) assert len(obj.b_dict) == len(obj4.b_dict) def test_enum_serialization(self): e = EnumTest.a d = e.as_dict() e_new = EnumTest.from_dict(d) assert e_new.name == e.name assert e_new.value == e.value d = {"123": EnumTest.a} f = jsanitize(d) assert f["123"]["@module"] == "tests.test_json" assert f["123"]["@class"] == "EnumTest" assert f["123"]["value"] == 1 f = jsanitize(d, strict=True) assert f["123"]["@module"] == "tests.test_json" assert f["123"]["@class"] == "EnumTest" assert f["123"]["value"] == 1 f = jsanitize(d, strict=True, enum_values=True) assert f["123"] == 1 f = jsanitize(d, enum_values=True) assert f["123"] == 1 def test_enum_serialization_no_msonable(self): d = {"123": EnumNoAsDict.name_a} f = jsanitize(d) assert f["123"]["@module"] == "tests.test_json" assert f["123"]["@class"] == "EnumNoAsDict" assert f["123"]["value"] == "value_a" f = jsanitize(d, strict=True) assert f["123"]["@module"] == "tests.test_json" assert f["123"]["@class"] == "EnumNoAsDict" assert f["123"]["value"] == "value_a" f = jsanitize(d, strict=True, enum_values=True) assert f["123"] == "value_a" f = jsanitize(d, enum_values=True) assert f["123"] == "value_a" def test_save_load(self, tmp_path): """Tests the save and load serialization methods.""" test_good_class = GoodMSONClass( "Hello", "World", "Python", **{ "cant_serialize_me": GoodNOTMSONClass( "Hello2", "World2", "Python2", **{"values": []} ), "cant_serialize_me2": [ GoodNOTMSONClass("Hello4", "World4", "Python4", **{"values": []}), GoodNOTMSONClass("Hello4", "World4", "Python4", **{"values": []}), ], "cant_serialize_me3": [ { "tmp": GoodMSONClass( "Hello5", "World5", "Python5", **{"values": []} ), "tmp2": 2, "tmp3": [1, 2, 3], }, { "tmp5": GoodNOTMSONClass( "aHello5", "aWorld5", "aPython5", **{"values": []} ), "tmp2": 5, "tmp3": {"test": "test123"}, }, # Gotta check that if I hide an MSONable class somewhere # it still gets correctly serialized. {"actually_good": GoodMSONClass("1", "2", "3", **{"values": []})}, ], "values": [], }, ) # This will pass test_good_class.as_dict() # This will fail with pytest.raises(TypeError): test_good_class.to_json() # This should also pass though target = tmp_path / "test.json" test_good_class.save(target, json_kwargs={"indent": 4, "sort_keys": True}) # This will fail with pytest.raises(FileExistsError): test_good_class.save(target, strict=True) # Now check that reloading this, the classes are equal! test_good_class2 = GoodMSONClass.load(target) # Final check using load test_good_class3 = load(target) assert test_good_class == test_good_class2 assert test_good_class == test_good_class3 class TestJson: def test_as_from_dict(self): obj = GoodMSONClass(1, 2, 3, hello="world") s = json.dumps(obj, cls=MontyEncoder) obj2 = json.loads(s, cls=MontyDecoder) assert obj2.a == 1 assert obj2.b == 2 assert obj2._c == 3 assert obj2._d == 1 assert obj2.kwargs, {"hello": "world", "values": []} obj = GoodMSONClass(obj, 2, 3) s = json.dumps(obj, cls=MontyEncoder) obj2 = json.loads(s, cls=MontyDecoder) assert obj2.a.a == 1 assert obj2.b == 2 assert obj2._c == 3 assert obj2._d == 1 listobj = [obj, obj2] s = json.dumps(listobj, cls=MontyEncoder) listobj2 = json.loads(s, cls=MontyDecoder) assert listobj2[0].a.a == 1 @pytest.mark.skipif(torch is None, reason="torch not present") def test_torch(self): t = torch.tensor([0, 1, 2]) jsonstr = json.dumps(t, cls=MontyEncoder) t2 = json.loads(jsonstr, cls=MontyDecoder) assert isinstance(t2, torch.Tensor) assert t2.type() == t.type() assert np.array_equal(t2, t) t = torch.tensor([1 + 1j, 2 + 1j]) jsonstr = json.dumps(t, cls=MontyEncoder) t2 = json.loads(jsonstr, cls=MontyDecoder) assert isinstance(t2, torch.Tensor) assert t2.type() == t.type() assert np.array_equal(t2, t) def test_datetime(self): dt = datetime.datetime.now() jsonstr = json.dumps(dt, cls=MontyEncoder) d = json.loads(jsonstr, cls=MontyDecoder) assert isinstance(d, datetime.datetime) assert dt == d # Test a nested datetime. a = {"dt": dt, "a": 1} jsonstr = json.dumps(a, cls=MontyEncoder) d = json.loads(jsonstr, cls=MontyDecoder) assert isinstance(d["dt"], datetime.datetime) jsanitize(dt, strict=True) # test timezone aware datetime API created_at = datetime.datetime.now(tz=datetime.timezone.utc) data = json.loads(json.dumps(created_at, cls=MontyEncoder)) created_at_after = MontyDecoder().process_decoded(data) assert str(created_at_after).rstrip("0") == str(created_at).rstrip( "+00:00" ).rstrip("0") def test_uuid(self): from uuid import UUID, uuid4 uuid = uuid4() jsonstr = json.dumps(uuid, cls=MontyEncoder) d = json.loads(jsonstr, cls=MontyDecoder) assert isinstance(d, UUID) assert uuid == d # Test a nested UUID. a = {"uuid": uuid, "a": 1} jsonstr = json.dumps(a, cls=MontyEncoder) d = json.loads(jsonstr, cls=MontyDecoder) assert isinstance(d["uuid"], UUID) def test_path(self): from pathlib import Path p = Path("/home/user/") jsonstr = json.dumps(p, cls=MontyEncoder) d = json.loads(jsonstr, cls=MontyDecoder) assert isinstance(d, Path) assert d == p def test_nan(self): x = [float("NaN")] djson = json.dumps(x, cls=MontyEncoder) d = json.loads(djson) assert isinstance(d[0], float) @pytest.mark.skipif(np is None, reason="numpy not present") def test_numpy(self): x = np.array([1, 2, 3], dtype="int64") with pytest.raises(TypeError): json.dumps(x) djson = json.dumps(x, cls=MontyEncoder) d = json.loads(djson) assert d["@class"] == "array" assert d["@module"] == "numpy" assert d["data"], [1, 2 == 3] assert d["dtype"] == "int64" x = json.loads(djson, cls=MontyDecoder) assert isinstance(x, np.ndarray) x = np.min([1, 2, 3]) > 2 with pytest.raises(TypeError): json.dumps(x) x = np.array([1 + 1j, 2 + 1j, 3 + 1j], dtype="complex64") with pytest.raises(TypeError): json.dumps(x) djson = json.dumps(x, cls=MontyEncoder) d = json.loads(djson) assert d["@class"] == "array" assert d["@module"] == "numpy" assert d["data"], [[1.0, 2.0, 3.0], [1.0, 1.0 == 1.0]] assert d["dtype"] == "complex64" x = json.loads(djson, cls=MontyDecoder) assert isinstance(x, np.ndarray) assert x.dtype == "complex64" x = np.array([[1 + 1j, 2 + 1j], [3 + 1j, 4 + 1j]], dtype="complex64") with pytest.raises(TypeError): json.dumps(x) djson = json.dumps(x, cls=MontyEncoder) d = json.loads(djson) assert d["@class"] == "array" assert d["@module"] == "numpy" assert d["data"], [[[1.0, 2.0], [3.0, 4.0]], [[1.0, 1.0], [1.0 == 1.0]]] assert d["dtype"] == "complex64" x = json.loads(djson, cls=MontyDecoder) assert isinstance(x, np.ndarray) assert x.dtype == "complex64" x = {"energies": [np.float64(1234.5)]} d = jsanitize(x, strict=True) assert isinstance(d["energies"][0], float) x = {"energy": np.array(-1.0)} d = jsanitize(x, strict=True) assert isinstance(d["energy"], float) # Test data nested in a class x = np.array([[1 + 1j, 2 + 1j], [3 + 1j, 4 + 1j]], dtype="complex64") cls = ClassContainingNumpyArray(np_a={"a": [{"b": x}]}) d = json.loads(json.dumps(cls, cls=MontyEncoder)) assert d["np_a"]["a"][0]["b"]["@module"] == "numpy" assert d["np_a"]["a"][0]["b"]["@class"] == "array" assert d["np_a"]["a"][0]["b"]["data"] == [ [[1.0, 2.0], [3.0, 4.0]], [[1.0, 1.0], [1.0, 1.0]], ] assert d["np_a"]["a"][0]["b"]["dtype"] == "complex64" obj = ClassContainingNumpyArray.from_dict(d) assert isinstance(obj, ClassContainingNumpyArray) assert isinstance(obj.np_a["a"][0]["b"], np.ndarray) assert obj.np_a["a"][0]["b"][0][1] == 2 + 1j @pytest.mark.skipif(pd is None, reason="pandas not present") def test_pandas(self): cls = ClassContainingDataFrame( df=pd.DataFrame([{"a": 1, "b": 1}, {"a": 1, "b": 2}]) ) d = json.loads(MontyEncoder().encode(cls)) assert d["df"]["@module"] == "pandas" assert d["df"]["@class"] == "DataFrame" obj = ClassContainingDataFrame.from_dict(d) assert isinstance(obj, ClassContainingDataFrame) assert isinstance(obj.df, pd.DataFrame) assert list(obj.df.a), [1 == 1] cls = ClassContainingSeries(s=pd.Series({"a": [1, 2, 3], "b": [4, 5, 6]})) d = json.loads(MontyEncoder().encode(cls)) assert d["s"]["@module"] == "pandas" assert d["s"]["@class"] == "Series" obj = ClassContainingSeries.from_dict(d) assert isinstance(obj, ClassContainingSeries) assert isinstance(obj.s, pd.Series) assert list(obj.s.a), [1, 2 == 3] cls = ClassContainingSeries( s={"df": [pd.Series({"a": [1, 2, 3], "b": [4, 5, 6]})]} ) d = json.loads(MontyEncoder().encode(cls)) assert d["s"]["df"][0]["@module"] == "pandas" assert d["s"]["df"][0]["@class"] == "Series" obj = ClassContainingSeries.from_dict(d) assert isinstance(obj, ClassContainingSeries) assert isinstance(obj.s["df"][0], pd.Series) assert list(obj.s["df"][0].a), [1, 2 == 3] @pytest.mark.skipif(pint is None, reason="pint not present") def test_pint_quantity(self): ureg = pint.UnitRegistry() cls = ClassContainingQuantity(qty=pint.Quantity("9.81 m/s**2")) d = json.loads(MontyEncoder().encode(cls)) print(d) assert d["qty"]["@module"] == "pint" assert d["qty"]["@class"] == "Quantity" assert d["qty"].get("@version") is not None obj = ClassContainingQuantity.from_dict(d) assert isinstance(obj, ClassContainingQuantity) assert isinstance(obj.qty, pint.Quantity) assert obj.qty.magnitude == 9.81 assert str(obj.qty.units) == "meter / second ** 2" def test_callable(self): instance = MethodSerializationClass(a=1) for function in [ # builtins str, list, sum, open, # functions os.path.join, my_callable, # unbound methods MethodSerializationClass.NestedClass.inner_method, MethodSerializationClass.staticmethod, instance.staticmethod, # methods bound to classes MethodSerializationClass.classmethod, instance.classmethod, # classes MethodSerializationClass, Enum, ]: with pytest.raises(TypeError): json.dumps(function) djson = json.dumps(function, cls=MontyEncoder) d = json.loads(djson) assert "@callable" in d assert "@module" in d x = json.loads(djson, cls=MontyDecoder) assert x == function # test method bound to instance for function in [instance.method]: with pytest.raises(TypeError): json.dumps(function) djson = json.dumps(function, cls=MontyEncoder) d = json.loads(djson) assert "@callable" in d assert "@module" in d x = json.loads(djson, cls=MontyDecoder) # can't just check functions are equal as the instance the function is bound # to will be different. Instead, we check that the serialized instance # is the same, and that the function qualname is the same assert x.__qualname__ == function.__qualname__ assert x.__self__.as_dict() == function.__self__.as_dict() # test method bound to object that is not serializable for function in [MethodNonSerializationClass(1).method]: with pytest.raises(TypeError): json.dumps(function, cls=MontyEncoder) # test that callable MSONable objects still get serialized as the objects # rather than as a callable djson = json.dumps(instance, cls=MontyEncoder) assert "@class" in djson @pytest.mark.skipif(ObjectId is None, reason="bson not present") def test_objectid(self): oid = ObjectId("562e8301218dcbbc3d7d91ce") with pytest.raises(TypeError): json.dumps(oid) djson = json.dumps(oid, cls=MontyEncoder) x = json.loads(djson, cls=MontyDecoder) assert isinstance(x, ObjectId) def test_jsanitize(self): # clean_json should have no effect on None types. d = {"hello": 1, "world": None} clean = jsanitize(d) assert clean["world"] is None assert json.loads(json.dumps(d)) == json.loads(json.dumps(clean)) d = {"hello": GoodMSONClass(1, 2, 3), "test": "hi"} with pytest.raises(TypeError): json.dumps(d) clean = jsanitize(d) assert isinstance(clean["hello"], str) clean_strict = jsanitize(d, strict=True) assert clean_strict["hello"]["a"] == 1 assert clean_strict["hello"]["b"] == 2 assert clean_strict["test"] == "hi" clean_recursive_msonable = jsanitize(d, recursive_msonable=True) assert clean_recursive_msonable["hello"]["a"] == 1 assert clean_recursive_msonable["hello"]["b"] == 2 assert clean_recursive_msonable["hello"]["c"] == 3 assert clean_recursive_msonable["test"] == "hi" d = {"hello": [GoodMSONClass(1, 2, 3), "test"], "test": "hi"} clean_recursive_msonable = jsanitize(d, recursive_msonable=True) assert clean_recursive_msonable["hello"][0]["a"] == 1 assert clean_recursive_msonable["hello"][0]["b"] == 2 assert clean_recursive_msonable["hello"][0]["c"] == 3 assert clean_recursive_msonable["hello"][1] == "test" assert clean_recursive_msonable["test"] == "hi" d = {"hello": (GoodMSONClass(1, 2, 3), "test"), "test": "hi"} clean_recursive_msonable = jsanitize(d, recursive_msonable=True) assert clean_recursive_msonable["hello"][0]["a"] == 1 assert clean_recursive_msonable["hello"][0]["b"] == 2 assert clean_recursive_msonable["hello"][0]["c"] == 3 assert clean_recursive_msonable["hello"][1] == "test" assert clean_recursive_msonable["test"] == "hi" d = {"dt": datetime.datetime.now()} clean = jsanitize(d) assert isinstance(clean["dt"], str) clean = jsanitize(d, allow_bson=True) assert isinstance(clean["dt"], datetime.datetime) rnd_bin = bytes(np.random.rand(10)) d = {"a": bytes(rnd_bin)} clean = jsanitize(d, allow_bson=True) assert clean["a"] == bytes(rnd_bin) assert isinstance(clean["a"], bytes) p = pathlib.Path("/home/user/") clean = jsanitize(p, strict=True) assert clean, ["/home/user" in "\\home\\user"] # test jsanitizing callables (including classes) instance = MethodSerializationClass(a=1) for function in [ # builtins str, list, sum, open, # functions os.path.join, my_callable, # unbound methods MethodSerializationClass.NestedClass.inner_method, MethodSerializationClass.staticmethod, instance.staticmethod, # methods bound to classes MethodSerializationClass.classmethod, instance.classmethod, # classes MethodSerializationClass, Enum, ]: d = {"f": function} clean = jsanitize(d) assert "@module" in clean["f"] assert "@callable" in clean["f"] # test method bound to instance for function in [instance.method]: d = {"f": function} clean = jsanitize(d) assert "@module" in clean["f"] assert "@callable" in clean["f"] assert clean["f"].get("@bound", None) is not None assert "@class" in clean["f"]["@bound"] # test method bound to object that is not serializable for function in [MethodNonSerializationClass(1).method]: d = {"f": function} clean = jsanitize(d) assert isinstance(clean["f"], str) # test that strict checking gives an error with pytest.raises(AttributeError): jsanitize(d, strict=True) # test that callable MSONable objects still get serialized as the objects # rather than as a callable d = {"c": instance} clean = jsanitize(d, strict=True) assert "@class" in clean["c"] @pytest.mark.skipif(pd is None, reason="pandas not present") def test_jsanitize_pandas(self): s = pd.Series({"a": [1, 2, 3], "b": [4, 5, 6]}) clean = jsanitize(s) assert clean == s.to_dict() @pytest.mark.skipif( np is None or ObjectId is None, reason="numpy and bson not present" ) def test_jsanitize_numpy_bson(self): d = { "a": ["b", np.array([1, 2, 3])], "b": ObjectId.from_datetime(datetime.datetime.now()), } clean = jsanitize(d) assert clean["a"], ["b", [1, 2 == 3]] assert isinstance(clean["b"], str) def test_redirect(self): MSONable.REDIRECT["tests.test_json"] = { "test_class": {"@class": "GoodMSONClass", "@module": "tests.test_json"}, "another_test_class": { "@class": "AnotherClass", "@module": "tests.test_json2", }, } d = { "@class": "test_class", "@module": "tests.test_json", "a": 1, "b": 1, "c": 1, } obj = json.loads(json.dumps(d), cls=MontyDecoder) assert isinstance(obj, GoodMSONClass) d2 = { "@class": "another_test_class", "@module": "tests.test_json", "a": 2, "b": 2, "c": 2, } with pytest.raises(ImportError, match="No module named 'tests.test_json2'"): # This should raise ImportError because it's trying to load # AnotherClass from tests.test_json instead of tests.test_json2 json.loads(json.dumps(d2), cls=MontyDecoder) def test_redirect_settings_file(self): data = _load_redirect(os.path.join(TEST_DIR, "test_settings.yaml")) assert data == { "old_module": { "old_class": {"@class": "new_class", "@module": "new_module"} } } @pytest.mark.skipif(pydantic is None, reason="pydantic not present") def test_pydantic_integrations(self): from pydantic import BaseModel, ValidationError global ModelWithMSONable # allow model to be deserialized in test global LimitedMSONClass class ModelWithMSONable(BaseModel): a: GoodMSONClass test_object = ModelWithMSONable(a=GoodMSONClass(1, 1, 1)) test_dict_object = ModelWithMSONable(a=test_object.a.as_dict()) assert test_dict_object.a.a == test_object.a.a dict_no_class = test_object.a.as_dict() dict_no_class.pop("@class") dict_no_class.pop("@module") test_dict_object = ModelWithMSONable(a=dict_no_class) assert test_dict_object.a.a == test_object.a.a assert test_object.model_json_schema() == { "title": "ModelWithMSONable", "type": "object", "properties": { "a": { "title": "A", "type": "object", "properties": { "@class": {"enum": ["GoodMSONClass"], "type": "string"}, "@module": {"enum": ["tests.test_json"], "type": "string"}, "@version": {"type": "string"}, }, "required": ["@class", "@module"], } }, "required": ["a"], } d = jsanitize(test_object, strict=True, enum_values=True, allow_bson=True) assert d == { "a": { "@module": "tests.test_json", "@class": "GoodMSONClass", "@version": "0.1", "a": 1, "b": 1, "c": 1, "d": 1, "values": [], }, "@module": "tests.test_json", "@class": "ModelWithMSONable", "@version": "0.1", } obj = MontyDecoder().process_decoded(d) assert isinstance(obj, BaseModel) assert isinstance(obj.a, GoodMSONClass) assert obj.a.b == 1 # check that a model that is not validated by pydantic still # gets completely deserialized. global ModelWithDict # allow model to be deserialized in test class ModelWithDict(BaseModel): a: dict test_object_with_dict = ModelWithDict(a={"x": GoodMSONClass(1, 1, 1)}) d = jsanitize( test_object_with_dict, strict=True, enum_values=True, allow_bson=True ) assert d == { "a": { "x": { "@module": "tests.test_json", "@class": "GoodMSONClass", "@version": "0.1", "a": 1, "b": 1, "c": 1, "d": 1, "values": [], } }, "@module": "tests.test_json", "@class": "ModelWithDict", "@version": "0.1", } obj = MontyDecoder().process_decoded(d) assert isinstance(obj, BaseModel) assert isinstance(obj.a["x"], GoodMSONClass) assert obj.a["x"].b == 1 # check that if an MSONable object raises an exception during # the model validation it is properly handled by pydantic global ModelWithUnion # allow model to be deserialized in test global ModelWithLimited # allow model to be deserialized in test class ModelWithLimited(BaseModel): a: LimitedMSONClass class ModelWithUnion(BaseModel): a: Union[LimitedMSONClass, dict] limited_dict = jsanitize(ModelWithLimited(a=LimitedMSONClass(1)), strict=True) assert ModelWithLimited.model_validate(limited_dict) limited_dict["a"]["b"] = 2 with pytest.raises(ValidationError): ModelWithLimited.model_validate(limited_dict) limited_union_dict = jsanitize( ModelWithUnion(a=LimitedMSONClass(1)), strict=True ) validated_model = ModelWithUnion.model_validate(limited_union_dict) assert isinstance(validated_model, ModelWithUnion) assert isinstance(validated_model.a, LimitedMSONClass) limited_union_dict["a"]["b"] = 2 validated_model = ModelWithUnion.model_validate(limited_union_dict) assert isinstance(validated_model, ModelWithUnion) assert isinstance(validated_model.a, dict) def test_dataclass(self): c = Coordinates([Point(1, 2), Point(3, 4)]) d = c.as_dict() c2 = Coordinates.from_dict(d) assert d["points"][0]["x"] == 1 assert d["points"][1]["y"] == 4 assert isinstance(c2, Coordinates) assert isinstance(c2.points[0], Point) s = MontyEncoder().encode(Point(1, 2)) p = MontyDecoder().decode(s) assert p.x == 1 assert p.y == 2 ndc = NestedDataClass([Point(1, 2), Point(3, 4)]) str_ = json.dumps(ndc, cls=MontyEncoder) ndc2 = json.loads(str_, cls=MontyDecoder) assert isinstance(ndc2, NestedDataClass) def test_enum(self): s = MontyEncoder().encode(EnumNoAsDict.name_a) p = MontyDecoder().decode(s) assert p.name == "name_a" assert p.value == "value_a" na1 = EnumAsDict.name_a d_ = na1.as_dict() assert d_ == {"v": "value_a"} na2 = EnumAsDict.from_dict(d_) assert na2 == na1 monty-2024.7.29/tests/test_logging.py000066400000000000000000000004061465166210500174160ustar00rootroot00000000000000from __future__ import annotations import logging from io import StringIO from monty.logging import logged @logged() def add(a, b): return a + b def test_logged(): s = StringIO() logging.basicConfig(level=logging.DEBUG, stream=s) add(1, 2) monty-2024.7.29/tests/test_math.py000066400000000000000000000002351465166210500167210ustar00rootroot00000000000000from __future__ import annotations from monty.math import nCr, nPr def test_nCr(): assert nCr(4, 2) == 6 def test_nPr(): assert nPr(4, 2) == 12 monty-2024.7.29/tests/test_multiprocessing.py000066400000000000000000000007201465166210500212160ustar00rootroot00000000000000from __future__ import annotations from math import sqrt from monty.multiprocessing import imap_tqdm def test_imap_tqdm(): results = imap_tqdm(4, sqrt, range(10000)) assert len(results) == 10000 assert results[0] == 0 assert results[400] == 20 assert results[9999] == 99.99499987499375 results = imap_tqdm(4, sqrt, (i**2 for i in range(10000))) assert len(results) == 10000 assert results[0] == 0 assert results[400] == 400 monty-2024.7.29/tests/test_operator.py000066400000000000000000000003071465166210500176230ustar00rootroot00000000000000from __future__ import annotations from monty.operator import operator_from_str def test_operator_from_str(): assert operator_from_str("==")(1, 1) assert operator_from_str("+")(1, 1) == 2 monty-2024.7.29/tests/test_os.py000066400000000000000000000033651465166210500164200ustar00rootroot00000000000000from __future__ import annotations import os from pathlib import Path import pytest from monty.os import cd, makedirs_p from monty.os.path import find_exts, zpath MODULE_DIR = os.path.dirname(__file__) TEST_DIR = os.path.join(MODULE_DIR, "test_files") class TestPath: def test_zpath(self, tmp_path: Path): tmp_gz = tmp_path / "tmp.gz" tmp_gz.touch() ret_path = zpath(str(tmp_gz)) assert ret_path == str(tmp_gz) tmp_not_bz2 = tmp_path / "tmp_not_bz2" tmp_not_bz2.touch() ret_path = zpath(f"{tmp_not_bz2}.bz2") assert ret_path == str(tmp_not_bz2) def test_find_exts(self): assert len(find_exts(MODULE_DIR, "py")) >= 18 assert len(find_exts(MODULE_DIR, "bz2")) == 2 n_bz2_excl_tests = len(find_exts(MODULE_DIR, "bz2", exclude_dirs="test_files")) assert n_bz2_excl_tests == 0 n_bz2_w_tests = find_exts(MODULE_DIR, "bz2", include_dirs="test_files") assert len(n_bz2_w_tests) == 2 class TestCd: def test_cd(self): with cd(TEST_DIR): assert os.path.exists("empty_file.txt") assert not os.path.exists("empty_file.txt") def test_cd_exception(self): with cd(TEST_DIR): assert os.path.exists("empty_file.txt") assert not os.path.exists("empty_file.txt") class TestMakedirs_p: def setup_method(self): self.test_dir_path = os.path.join(TEST_DIR, "test_dir") def test_makedirs_p(self): makedirs_p(self.test_dir_path) assert os.path.exists(self.test_dir_path) makedirs_p(self.test_dir_path) with pytest.raises(OSError): makedirs_p(os.path.join(TEST_DIR, "myfile_txt")) def teardown_method(self): os.rmdir(self.test_dir_path) monty-2024.7.29/tests/test_pprint.py000066400000000000000000000016031465166210500173040ustar00rootroot00000000000000from __future__ import annotations from monty.pprint import draw_tree, pprint_table class TestPprintTable: def test_print(self): table = [["one", "two"], ["1", "2"]] pprint_table(table) class TestDrawTree: def test_draw_tree(self): class Node: def __init__(self, name, children): self.name = name self.children = children def __str__(self): return self.name root = Node( "root", [ Node("sub1", []), Node("sub2", [Node("sub2sub1", [])]), Node( "sub3", [ Node("sub3sub1", [Node("sub3sub1sub1", [])]), Node("sub3sub2", []), ], ), ], ) print(draw_tree(root)) monty-2024.7.29/tests/test_re.py000066400000000000000000000014561465166210500164040ustar00rootroot00000000000000from __future__ import annotations import os from monty.re import regrep TEST_DIR = os.path.join(os.path.dirname(__file__), "test_files") def test_regrep(): """ We are making sure a file containing line numbers is read in reverse order, i.e. the first line that is read corresponds to the last line. number """ fname = os.path.join(TEST_DIR, "3000_lines.txt") matches = regrep(fname, {"1": r"1(\d+)", "3": r"3(\d+)"}, postprocess=int) assert len(matches["1"]) == 1380 assert len(matches["3"]) == 571 assert matches["1"][0][0][0] == 0 matches = regrep( fname, {"1": r"1(\d+)", "3": r"3(\d+)"}, reverse=True, terminate_on_match=True, postprocess=int, ) assert len(matches["1"]) == 1 assert len(matches["3"]) == 11 monty-2024.7.29/tests/test_serialization.py000066400000000000000000000046531465166210500206550ustar00rootroot00000000000000from __future__ import annotations import glob import json import os import unittest import pytest try: import msgpack except ImportError: msgpack = None from monty.serialization import dumpfn, loadfn from monty.tempfile import ScratchDir class TestSerial: @classmethod def teardown_class(cls): # Cleans up test files if a test fails files_to_clean_up = glob.glob("monte_test.*") for fn in files_to_clean_up: os.remove(fn) def test_dumpfn_loadfn(self): d = {"hello": "world"} # Test standard configuration for ext in ( "json", "yaml", "yml", "json.gz", "yaml.gz", "json.bz2", "yaml.bz2", ): fn = f"monte_test.{ext}" dumpfn(d, fn) d2 = loadfn(fn) assert d == d2, f"Test file with extension {ext} did not parse correctly" os.remove(fn) # Test custom kwarg configuration dumpfn(d, "monte_test.json", indent=4) d2 = loadfn("monte_test.json") assert d == d2 os.remove("monte_test.json") dumpfn(d, "monte_test.yaml") d2 = loadfn("monte_test.yaml") assert d == d2 os.remove("monte_test.yaml") # Check if fmt override works. dumpfn(d, "monte_test.json", fmt="yaml") with pytest.raises(json.decoder.JSONDecodeError): loadfn("monte_test.json") d2 = loadfn("monte_test.json", fmt="yaml") assert d == d2 os.remove("monte_test.json") with pytest.raises(TypeError): dumpfn(d, "monte_test.txt", fmt="garbage") with pytest.raises(TypeError): loadfn("monte_test.txt", fmt="garbage") @unittest.skipIf(msgpack is None, "msgpack-python not installed.") def test_mpk(self): d = {"hello": "world"} # Test automatic format detection dumpfn(d, "monte_test.mpk") d2 = loadfn("monte_test.mpk") assert d == d2 os.remove("monte_test.mpk") # Test to ensure basename is respected, and not directory with ScratchDir("."): os.mkdir("mpk_test") os.chdir("mpk_test") fname = os.path.abspath("test_file.json") dumpfn({"test": 1}, fname) with open("test_file.json") as f: reloaded = json.loads(f.read()) assert reloaded["test"] == 1 monty-2024.7.29/tests/test_shutil.py000066400000000000000000000156501465166210500173070ustar00rootroot00000000000000from __future__ import annotations import os import platform import shutil import tempfile import unittest from gzip import GzipFile from pathlib import Path import pytest from monty.shutil import ( compress_dir, compress_file, copy_r, decompress_dir, decompress_file, gzip_dir, remove, ) test_dir = os.path.join(os.path.dirname(__file__), "test_files") class TestCopyR: def setup_method(self): os.mkdir(os.path.join(test_dir, "cpr_src")) with open(os.path.join(test_dir, "cpr_src", "test"), "w") as f: f.write("what") os.mkdir(os.path.join(test_dir, "cpr_src", "sub")) with open(os.path.join(test_dir, "cpr_src", "sub", "testr"), "w") as f: f.write("what2") if os.name != "nt": os.symlink( os.path.join(test_dir, "cpr_src", "test"), os.path.join(test_dir, "cpr_src", "mysymlink"), ) def test_recursive_copy_and_compress(self): copy_r(os.path.join(test_dir, "cpr_src"), os.path.join(test_dir, "cpr_dst")) assert os.path.exists(os.path.join(test_dir, "cpr_dst", "test")) assert os.path.exists(os.path.join(test_dir, "cpr_dst", "sub", "testr")) compress_dir(os.path.join(test_dir, "cpr_src")) assert os.path.exists(os.path.join(test_dir, "cpr_src", "test.gz")) assert os.path.exists(os.path.join(test_dir, "cpr_src", "sub", "testr.gz")) decompress_dir(os.path.join(test_dir, "cpr_src")) assert os.path.exists(os.path.join(test_dir, "cpr_src", "test")) assert os.path.exists(os.path.join(test_dir, "cpr_src", "sub", "testr")) with open(os.path.join(test_dir, "cpr_src", "test")) as f: txt = f.read() assert txt == "what" def test_pathlib(self): test_path = Path(test_dir) copy_r(test_path / "cpr_src", test_path / "cpr_dst") assert (test_path / "cpr_dst" / "test").exists() assert (test_path / "cpr_dst" / "sub" / "testr").exists() def teardown_method(self): shutil.rmtree(os.path.join(test_dir, "cpr_src")) shutil.rmtree(os.path.join(test_dir, "cpr_dst")) class TestCompressFileDir: def setup_method(self): with open(os.path.join(test_dir, "tempfile"), "w") as f: f.write("hello world") def test_compress_and_decompress_file(self): fname = os.path.join(test_dir, "tempfile") for fmt in ["gz", "bz2"]: compress_file(fname, fmt) assert os.path.exists(fname + "." + fmt) assert not os.path.exists(fname) decompress_file(fname + "." + fmt) assert os.path.exists(fname) assert not os.path.exists(fname + "." + fmt) with open(fname) as f: assert f.read() == "hello world" with pytest.raises(ValueError): compress_file("whatever", "badformat") # test decompress non-existent/non-compressed file assert decompress_file("non-existent") is None assert decompress_file("non-existent.gz") is None assert decompress_file("non-existent.bz2") is None def test_compress_and_decompress_with_target_dir(self): fname = os.path.join(test_dir, "tempfile") target_dir = os.path.join(test_dir, "temp_target_dir") for fmt in ["gz", "bz2"]: compress_file(fname, fmt, target_dir) compressed_file_path = os.path.join( target_dir, f"{os.path.basename(fname)}.{fmt}" ) assert os.path.exists(compressed_file_path) assert not os.path.exists(fname) decompress_file(compressed_file_path, target_dir) decompressed_file_path = os.path.join(target_dir, os.path.basename(fname)) assert os.path.exists(decompressed_file_path) assert not os.path.exists(compressed_file_path) # Reset temp file position shutil.move(decompressed_file_path, fname) shutil.rmtree(target_dir) with open(fname) as f: assert f.read() == "hello world" def teardown_method(self): os.remove(os.path.join(test_dir, "tempfile")) class TestGzipDir: def setup_method(self): os.mkdir(os.path.join(test_dir, "gzip_dir")) with open(os.path.join(test_dir, "gzip_dir", "tempfile"), "w") as f: f.write("what") self.mtime = os.path.getmtime(os.path.join(test_dir, "gzip_dir", "tempfile")) def test_gzip(self): full_f = os.path.join(test_dir, "gzip_dir", "tempfile") gzip_dir(os.path.join(test_dir, "gzip_dir")) assert os.path.exists(f"{full_f}.gz") assert not os.path.exists(full_f) with GzipFile(f"{full_f}.gz") as g: assert g.readline().decode("utf-8") == "what" assert os.path.getmtime(f"{full_f}.gz") == pytest.approx(self.mtime, 4) def test_handle_sub_dirs(self): sub_dir = os.path.join(test_dir, "gzip_dir", "sub_dir") sub_file = os.path.join(sub_dir, "new_tempfile") os.mkdir(sub_dir) with open(sub_file, "w") as f: f.write("anotherwhat") gzip_dir(os.path.join(test_dir, "gzip_dir")) assert os.path.exists(f"{sub_file}.gz") assert not os.path.exists(sub_file) with GzipFile(f"{sub_file}.gz") as g: assert g.readline().decode("utf-8") == "anotherwhat" def teardown_method(self): shutil.rmtree(os.path.join(test_dir, "gzip_dir")) class TestRemove: @unittest.skipIf(platform.system() == "Windows", "Skip on windows") def test_remove_file(self): tempdir = tempfile.mkdtemp(dir=test_dir) tempf = tempfile.mkstemp(dir=tempdir)[1] remove(tempf) assert not os.path.isfile(tempf) shutil.rmtree(tempdir) @unittest.skipIf(platform.system() == "Windows", "Skip on windows") def test_remove_folder(self): tempdir = tempfile.mkdtemp(dir=test_dir) remove(tempdir) assert not os.path.isdir(tempdir) @unittest.skipIf(platform.system() == "Windows", "Skip on windows") def test_remove_symlink(self): tempdir = tempfile.mkdtemp(dir=test_dir) tempf = tempfile.mkstemp(dir=tempdir)[1] os.symlink(tempdir, os.path.join(test_dir, "temp_link")) templink = os.path.join(test_dir, "temp_link") remove(templink) assert os.path.isfile(tempf) assert os.path.isdir(tempdir) assert not os.path.islink(templink) remove(tempdir) @unittest.skipIf(platform.system() == "Windows", "Skip on windows") def test_remove_symlink_follow(self): tempdir = tempfile.mkdtemp(dir=test_dir) tempf = tempfile.mkstemp(dir=tempdir)[1] os.symlink(tempdir, os.path.join(test_dir, "temp_link")) templink = os.path.join(test_dir, "temp_link") remove(templink, follow_symlink=True) assert not os.path.isfile(tempf) assert not os.path.isdir(tempdir) assert not os.path.islink(templink) monty-2024.7.29/tests/test_string.py000066400000000000000000000005371465166210500173030ustar00rootroot00000000000000""" TODO: Modify unittest doc. """ from __future__ import annotations import random from monty.string import remove_non_ascii def test_remove_non_ascii(): s = "".join(chr(random.randint(0, 127)) for i in range(10)) s += "".join(chr(random.randint(128, 150)) for i in range(10)) clean = remove_non_ascii(s) assert len(clean) == 10 monty-2024.7.29/tests/test_subprocess.py000066400000000000000000000005441465166210500201630ustar00rootroot00000000000000from __future__ import annotations from monty.subprocess import Command def test_command(): """Test Command class""" sleep05 = Command("sleep 0.5") sleep05.run(timeout=1) print(sleep05) assert sleep05.retcode == 0 assert not sleep05.killed sleep05.run(timeout=0.1) assert sleep05.retcode != 0 assert sleep05.killed monty-2024.7.29/tests/test_tempfile.py000066400000000000000000000112561465166210500176020ustar00rootroot00000000000000from __future__ import annotations import os import shutil from monty.tempfile import ScratchDir TEST_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "test_files") class TestScratchDir: def setup_method(self): self.cwd = os.getcwd() os.chdir(TEST_DIR) self.scratch_root = os.path.join(TEST_DIR, "..", "..", "tempscratch") os.mkdir(self.scratch_root) def test_with_copy(self): # We write a pre-scratch file. with open("pre_scratch_text", "w") as f: f.write("write") with ScratchDir( self.scratch_root, copy_from_current_on_enter=True, copy_to_current_on_exit=True, ) as d: with open("scratch_text", "w") as f: f.write("write") files = os.listdir(d) assert "scratch_text" in files assert "empty_file.txt" in files assert "pre_scratch_text" in files # We remove the pre-scratch file. os.remove("pre_scratch_text") # Make sure the tempdir is deleted. assert not os.path.exists(d) files = os.listdir(".") assert "scratch_text" in files # We check that the pre-scratch file no longer exists (because it is # deleted in the scratch) assert "pre_scratch_text" not in files os.remove("scratch_text") def test_with_copy_gzip(self): # We write a pre-scratch file. with open("pre_scratch_text", "w") as f: f.write("write") init_gz = [f for f in os.listdir(os.getcwd()) if f.endswith(".gz")] with ( ScratchDir( self.scratch_root, copy_from_current_on_enter=True, copy_to_current_on_exit=True, gzip_on_exit=True, ), open("scratch_text", "w") as f, ): f.write("write") files = os.listdir(os.getcwd()) # Make sure the stratch_text.gz exists assert "scratch_text.gz" in files for f in files: if f.endswith(".gz") and f not in init_gz: os.remove(f) os.remove("pre_scratch_text") def test_with_copy_nodelete(self): # We write a pre-scratch file. with open("pre_scratch_text", "w") as f: f.write("write") with ScratchDir( self.scratch_root, copy_from_current_on_enter=True, copy_to_current_on_exit=True, delete_removed_files=False, ) as d: with open("scratch_text", "w") as f: f.write("write") files = os.listdir(d) assert "scratch_text" in files assert "empty_file.txt" in files assert "pre_scratch_text" in files # We remove the pre-scratch file. os.remove("pre_scratch_text") # Make sure the tempdir is deleted. assert not os.path.exists(d) files = os.listdir(".") assert "scratch_text" in files # We check that the pre-scratch file DOES still exists assert "pre_scratch_text" in files os.remove("scratch_text") os.remove("pre_scratch_text") def test_no_copy(self): with ScratchDir( self.scratch_root, copy_from_current_on_enter=False, copy_to_current_on_exit=False, ) as d: with open("scratch_text", "w") as f: f.write("write") files = os.listdir(d) assert "scratch_text" in files assert "empty_file.txt" not in files # Make sure the tempdir is deleted. assert not os.path.exists(d) files = os.listdir(".") assert "scratch_text" not in files def test_symlink(self): if os.name != "nt": with ScratchDir( self.scratch_root, copy_from_current_on_enter=False, copy_to_current_on_exit=False, create_symbolic_link=True, ) as d: with open("scratch_text", "w") as f: f.write("write") files = os.listdir(d) assert "scratch_text" in files assert "empty_file.txt" not in files # Make sure the tempdir is deleted. assert not os.path.exists(d) files = os.listdir(".") assert "scratch_text" not in files # Make sure the symlink is removed assert not os.path.islink("scratch_link") def test_bad_root(self): with ScratchDir("bad_groot") as d: assert d == TEST_DIR def teardown_method(self): os.chdir(self.cwd) shutil.rmtree(self.scratch_root) monty-2024.7.29/tests/test_termcolor.py000066400000000000000000000047771465166210500200150ustar00rootroot00000000000000""" TODO: Modify unittest doc. """ from __future__ import annotations import os import sys from monty.termcolor import ( cprint, cprint_map, enable, get_terminal_size, stream_has_colours, ) class TestFunc: def test_remove_non_ascii(self): enable(True) print("Current terminal type: %s" % os.getenv("TERM")) print("Test basic colors:") cprint("Grey color", "grey") cprint("Red color", "red") cprint("Green color", "green") cprint("Yellow color", "yellow") cprint("Blue color", "blue") cprint("Magenta color", "magenta") cprint("Cyan color", "cyan") cprint("White color", "white") print("-" * 78) print("Test highlights:") cprint("On grey color", on_color="on_grey") cprint("On red color", on_color="on_red") cprint("On green color", on_color="on_green") cprint("On yellow color", on_color="on_yellow") cprint("On blue color", on_color="on_blue") cprint("On magenta color", on_color="on_magenta") cprint("On cyan color", on_color="on_cyan") cprint("On white color", color="grey", on_color="on_white") print("-" * 78) print("Test attributes:") cprint("Bold grey color", "grey", attrs=["bold"]) cprint("Dark red color", "red", attrs=["dark"]) cprint("Underline green color", "green", attrs=["underline"]) cprint("Blink yellow color", "yellow", attrs=["blink"]) cprint("Reversed blue color", "blue", attrs=["reverse"]) cprint("Concealed Magenta color", "magenta", attrs=["concealed"]) cprint( "Bold underline reverse cyan color", "cyan", attrs=["bold", "underline", "reverse"], ) cprint( "Dark blink concealed white color", "white", attrs=["dark", "blink", "concealed"], ) print("-" * 78) print("Test mixing:") cprint("Underline red on grey color", "red", "on_grey", ["underline"]) cprint("Reversed green on red color", "green", "on_red", ["reverse"]) # Test cprint_keys cprint_map("Hello world", {"Hello": "red"}) cprint_map("Hello world", {"Hello": {"color": "blue", "on_color": "on_red"}}) # Test terminal size. print("terminal size: %s", get_terminal_size()) enable(False) def test_stream_has_colors(self): # TODO: not a real test. Need to do a proper test. stream_has_colours(sys.stdout)