From 161d40387844a01a6ba4f45fdd53ad00345de32f Mon Sep 17 00:00:00 2001 From: Douglas Lassance Date: Mon, 29 Nov 2021 14:25:26 -0800 Subject: [PATCH] Carry initial cleanup --- .editorconfig | 25 +++ .flake8 | 3 + .github/workflows/cd.yaml | 27 +++ .github/workflows/ci.yaml | 37 ++++ .gitignore | 139 +++++++++++++++ .pylintrc | 10 ++ README.md | 119 +++++++++++++ README.rst | 149 ---------------- docs/Makefile | 20 +++ docs/make.bat | 35 ++++ docs/source/conf.py | 168 ++++++++++++++++++ docs/source/index.rst | 14 ++ gitmodel/__info__.py | 5 + gitmodel/__init__.py | 2 +- gitmodel/fields.py | 38 ++-- gitmodel/models.py | 23 +-- gitmodel/serializers/json.py | 2 +- gitmodel/test/__init__.py | 111 ------------ gitmodel/utils/__init__.py | 2 +- gitmodel/utils/dict.py | 2 +- gitmodel/utils/encoding.py | 97 ---------- gitmodel/utils/path.py | 12 +- gitmodel/workspace.py | 10 +- pyrightconfig.json | 5 + python-gitmodel.sublime-project | 63 +++++++ requirements-ci.txt | 1 + run-tests.py | 10 -- setup.py | 55 ++++-- tests/__init__.py | 36 ++++ {gitmodel/test => tests}/fields/__init__.py | 0 .../test => tests}/fields/git-logo-2color.png | Bin {gitmodel/test => tests}/fields/models.py | 0 {gitmodel/test => tests}/model/__init__.py | 0 .../test => tests}/model/diff_branch.diff | 0 .../test => tests}/model/diff_nobranch.diff | 0 {gitmodel/test => tests}/model/models.py | 2 +- .../model/tests.py => tests/test_model.py | 26 +-- {gitmodel/test => tests}/test_utils.py | 30 ++-- {gitmodel/test => tests}/test_workspace.py | 46 +++-- {gitmodel/test/fields => tests}/tests.py | 18 +- 40 files changed, 855 insertions(+), 487 deletions(-) create mode 100644 .editorconfig create mode 100644 .flake8 create mode 100644 .github/workflows/cd.yaml create mode 100644 .github/workflows/ci.yaml create mode 100644 .gitignore create mode 100644 .pylintrc create mode 100644 README.md delete mode 100644 README.rst create mode 100644 docs/Makefile create mode 100644 docs/make.bat create mode 100644 docs/source/conf.py create mode 100644 docs/source/index.rst create mode 100644 gitmodel/__info__.py delete mode 100644 gitmodel/test/__init__.py create mode 100644 pyrightconfig.json create mode 100644 python-gitmodel.sublime-project create mode 100644 requirements-ci.txt delete mode 100755 run-tests.py create mode 100644 tests/__init__.py rename {gitmodel/test => tests}/fields/__init__.py (100%) rename {gitmodel/test => tests}/fields/git-logo-2color.png (100%) rename {gitmodel/test => tests}/fields/models.py (100%) rename {gitmodel/test => tests}/model/__init__.py (100%) rename {gitmodel/test => tests}/model/diff_branch.diff (100%) rename {gitmodel/test => tests}/model/diff_nobranch.diff (100%) rename {gitmodel/test => tests}/model/models.py (95%) rename gitmodel/test/model/tests.py => tests/test_model.py (95%) rename {gitmodel/test => tests}/test_utils.py (87%) rename {gitmodel/test => tests}/test_workspace.py (80%) rename {gitmodel/test/fields => tests}/tests.py (96%) diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..965e91b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,25 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true + +# Python +[*.py] +indent_style = space +indent_size = 4 +max_line_length = 88 + +# YAML +[*.yaml] +indent_style = space +indent_size = 2 + +# JSON +[*.json] +indent_style = space +indent_size = 4 diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..06a8ccc --- /dev/null +++ b/.flake8 @@ -0,0 +1,3 @@ +[flake8] +max-line-length = 88 +exclude = .git,__pycache__,docs/source/conf.py,old,build,dist \ No newline at end of file diff --git a/.github/workflows/cd.yaml b/.github/workflows/cd.yaml new file mode 100644 index 0000000..4d79f23 --- /dev/null +++ b/.github/workflows/cd.yaml @@ -0,0 +1,27 @@ +name: CD + +on: + release: + types: [created] + +jobs: + deploy: + name: Deploy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: "3.x" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install setuptools wheel twine + - name: Publish on PyPI + env: + TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} + TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + run: | + python setup.py sdist bdist_wheel + twine upload --verbose dist/* diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..1a094e9 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,37 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + name: Test + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.7, 3.8, 3.9] + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install requirements + run: | + python -m pip install --upgrade pip + pip install --editable ".[ci]" + # - name: Lint with flake8 + # run: | + # flake8 . --count --show-source --statistics + - name: Test with pytest + run: | + pytest --cov=gitmodel + - name: Document with sphinx + run: | + sphinx-build ./docs/source ./docs/build + - name: Upload report on CodeCov + run: | + bash <(curl -s https://codecov.io/bash) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2704bd9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,139 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/build/ +docs/source/generated + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# Sublime Text +*.sublime-workspace + +# macOS +.DS_Store + +# IntelliJ IDEA +.idea/ diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..2627507 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,10 @@ +[MASTER] + +ignore=docs, __pycache__ +disable= + no-member, + too-many-arguments, + too-few-public-methods, + logging-format-interpolation, + missing-module-docstring + diff --git a/README.md b/README.md new file mode 100644 index 0000000..4b3fd09 --- /dev/null +++ b/README.md @@ -0,0 +1,119 @@ +# python-gitmodel + +[![PyPI version](https://badge.fury.io/py/gitarmony-python.svg)](https://badge.fury.io/py/gitarmony-python) +[![Documentation Status](https://readthedocs.org/projects/gitarmony-python/badge/?version=latest)](https://gitarmony-python.readthedocs.io/en/latest) +[![codecov](https://codecov.io/gh/bendavis78/python-gitmodel/branch/master/graph/badge.svg?token=5267NA3EQQ)](https://codecov.io/gh/bendavis78/python-gitmodel) + +## A distributed, versioned data store for Python + +python-gitmodel is a framework for persisting objects using Git for +versioning and remote syncing. + +## Why? + +According to [Git's README](), Git +is a \"stupid content tracker\". That means you aren\'t limited to +storing source code in git. The goal of this project is to provide an +object-level interface to use git as a schema-less data store, as well +as tools that take advantage of git\'s powerful versioning capabilities. + +python-gitmodel allows you to model your data using python, and provides +an easy-to-use interface for storing that data as git objects. + +python-gitmodel is based on [libgit2](), a +pure C implementation of the Git core methods. This means that instead +of calling git commands via shell, we get to use git at native speed. + +## What\'s so great about it? + +* Schema-less data store +* Never lose data. History is kept forever and can be restored using +* git tools. +* Branch and merge your production data + * gitmodel can work with different branches + * branch or tag snapshots of your data + * experiment on production data using branches, for example, to test a migration +* Ideal for content-driven applications + +## Example usage + +Below we\'ll cover a use-case for a basic flat-page CMS. + +Basic model creation: +```python + from gitmodel.workspace import Workspace from gitmodel import fields ws = Workspace('path/to/my-repo/.git') class Page(ws.GitModel): slug = fields.SlugField() title = fields.CharField() content = fields.CharField() published = fields.BooleanField(default=True) + ``` + +The Workspace can be thought of as your git working directory. It also +acts as the \"porcelain\" layer to pygit2\'s \"plumbing\". In contrast +to a working directory, the Workspace class does not make use of the +repository\'s INDEX and HEAD files, and instead keeps track of these in +memory. + +Saving objects: +```python + page = Page(slug='example-page', title='Example Page') page.content = '

Here is an Example

Lorem Ipsum

' page.save() print(page.id) # abc99c394ab546dd9d6e3381f9c0fb4b + ``` + +By default, objects get an auto-ID field which saves as a python UUID +hex (don\'t confuse these with git hashes). You can easily customize +which field in your model acts as the ID field, for example: +```python + class Page(ws.GitModel): slug = fields.SlugField(id=True) # OR class Page(ws.GitModel): slug = fields.SlugField() class Meta: id_field = 'slug' + ``` + +Objects are not committed to the repository by default. They are, +however, written into the object database as trees and blobs. The +[Workspace.index]{.title-ref} object is a [pygit2.Tree]{.title-ref} that +holds the uncommitted data. It\'s analagous to Git\'s index, except that +the pointer is stored in memory. + +Creating commits is simple: +```python + oid = page.save(commit=True, message='Added an example page') commit = ws.repo[oid] # a pygit2.Commit object print(commit.message) + ``` + +You can access previous commits using pygit2, and even view diffs +between two versions of an object. +```python + # walking commits for commit in ws.walk(): print("{}: {}".format(commit.hex, commit.message)) # get a diff between two commits head_commit = ws.branch.commit prev_commit_oid = head_commit.parents[0] print(prev_commit.diff(head_commit)) + ``` + +Objects can be easily retrieved by their id: +```python + page = Page.get('example-page') print(page.content) + ``` + +# Caveat Emptor + +Git doesn\'t perform very well on its own. If you need your git-backed +data to perform well in a production environment, you need to get it a +\"wingman\". Since python-gitmodel can be used in a variety of ways, +it\'s up to you to decide the best way to optimize it. + +# Status + +This project is no longer under active development. + +# TODO + +* Caching? +* Indexing? +* Query API? +* Full documentation + +python-gitmodel was inspired by Rick Olson\'s talk, \"[Git, the Stupid +NoSQL Database]()\" and Paul +Downman\'s [GitModel]() for +ruby. + +# Development + +This projects requires the following: + +- [Python >=3.7.9](https://www.python.org/downloads/release/python-379/) +- [virtualenwrapper](https://pypi.org/project/virtualenvwrapper/) (macOS/Linux) +- [virtualenwrapper-win](https://pypi.org/project/virtualenvwrapper-win/) (Windows) + +Make sure your `WORKON_HOME` environment variable is set on Windows, and create a `gitmodel` virtual environment with `mkvirtualenv`. +Build systems for installing requirements and running tests are on board of the SublimeText project. diff --git a/README.rst b/README.rst deleted file mode 100644 index eb16192..0000000 --- a/README.rst +++ /dev/null @@ -1,149 +0,0 @@ -=============== -python-gitmodel -=============== -A distributed, versioned data store for Python ----------------------------------------------- - -python-gitmodel is a framework for persisting objects using Git for versioning -and remote syncing. - -Why? ----- -According to `Git's README`_, Git is a "stupid content tracker". That means you -aren't limited to storing source code in git. The goal of this project is to -provide an object-level interface to use git as a schema-less data store, as -well as tools that take advantage of git's powerful versioning capabilities. - -python-gitmodel allows you to model your data using python, and provides an -easy-to-use interface for storing that data as git objects. - -python-gitmodel is based on `libgit2`_, a pure C implementation of the Git core -methods. This means that instead of calling git commands via shell, we get -to use git at native speed. - -What's so great about it? -------------------------- -* Schema-less data store -* Never lose data. History is kept forever and can be restored using git tools. -* Branch and merge your production data - - * python-gitmodel can work with different branches - * branch or tag snapshots of your data - * experiment on production data using branches, for example, to test a migration - -* Ideal for content-driven applications - -Example usage -------------- -Below we'll cover a use-case for a basic flat-page CMS. - -Basic model creation: - -.. code:: python - - from gitmodel.workspace import Workspace - from gitmodel import fields - - ws = Workspace('path/to/my-repo/.git') - - class Page(ws.GitModel): - slug = fields.SlugField() - title = fields.CharField() - content = fields.CharField() - published = fields.BooleanField(default=True) - -The Workspace can be thought of as your git working directory. It also acts as -the "porcelain" layer to pygit2's "plumbing". In contrast to a working -directory, the Workspace class does not make use of the repository's INDEX and -HEAD files, and instead keeps track of these in memory. - -Saving objects: - -.. code:: python - - page = Page(slug='example-page', title='Example Page') - page.content = '

Here is an Example

Lorem Ipsum

' - page.save() - - print(page.id) - # abc99c394ab546dd9d6e3381f9c0fb4b - -By default, objects get an auto-ID field which saves as a python UUID hex -(don't confuse these with git hashes). You can easily customize which field in -your model acts as the ID field, for example: - -.. code:: python - - class Page(ws.GitModel): - slug = fields.SlugField(id=True) - - # OR - - class Page(ws.GitModel): - slug = fields.SlugField() - - class Meta: - id_field = 'slug' - -Objects are not committed to the repository by default. They are, however, -written into the object database as trees and blobs. The ``Workspace.index`` -object is a ``pygit2.Tree`` that holds the uncommitted data. It's analagous to -Git's index, except that the pointer is stored in memory. - -Creating commits is simple: - -.. code:: python - - oid = page.save(commit=True, message='Added an example page') - commit = ws.repo[oid] # a pygit2.Commit object - print(commit.message) - -You can access previous commits using pygit2, and even view diffs between two -versions of an object. - -.. code:: python - - # walking commits - for commit in ws.walk(): - print("{}: {}".format(commit.hex, commit.message)) - - # get a diff between two commits - head_commit = ws.branch.commit - prev_commit_oid = head_commit.parents[0] - print(prev_commit.diff(head_commit)) - -Objects can be easily retrieved by their id: - -.. code:: python - - page = Page.get('example-page') - print(page.content) - - -Caveat Emptor -------------- -Git doesn't perform very well on its own. If you need your git-backed data to -perform well in a production environment, you need to get it a "wingman". -Since python-gitmodel can be used in a variety of ways, it's up to you to -decide the best way to optimize it. - -Status ------- -This project is no longer under active development. - -TODO ----- -* Caching? -* Indexing? -* Query API? -* Full documentation - -------------------------------------------------------------------------------- - -python-gitmodel was inspired by Rick Olson's talk, "`Git, the Stupid NoSQL -Database`_" and Paul Downman's `GitModel`_ for ruby. - -.. _Git's README: https://github.com/git/git#readme -.. _libgit2: http://libgit2.github.com -.. _Git, the Stupid NoSQL Database: http://git-nosql-rubyconf.heroku.com/ -.. _GitModel: https://github.com/pauldowman/gitmodel/ diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..1ecd382 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..9534b01 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000..9943b36 --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,168 @@ +# 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. +# +# pylint: skip-file + +import gitmodel + + +# -- Project information ----------------------------------------------------- + +project = gitmodel.__name__ +copyright = gitmodel.__copyright__ +author = gitmodel.__author__ + +# The full version, including alpha/beta/rc tags +release = gitmodel.__version__ + + +# -- 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. +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.autodoc", + "sphinx.ext.autosectionlabel", + "sphinx.ext.autosummary", + "sphinx.ext.coverage", + "sphinx.ext.doctest", + "sphinx.ext.githubpages", + "sphinx.ext.ifconfig", + "sphinx.ext.intersphinx", + "sphinx.ext.mathjax", + "sphinx.ext.napoleon", + "sphinx.ext.viewcode", + "sphinx_markdown_tables", + "sphinx_rtd_theme", + "sphinxcontrib.apidoc", +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + +# 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 = [ + "./docs", + "./tests", + "./setup.py", +] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +source_suffix = [".rst", ".md"] +# source_parsers = { +# ".md": "recommonmark.parser.CommonMarkParser", +# } + +add_module_names = False + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# import sphinx_rtd_theme +html_theme = "sphinx_rtd_theme" + +# 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 = [] + +html_theme_options = { + # "canonical_url": "", + # "logo_only": False, + # "display_version": True, + # "prev_next_buttons_location": "bottom", + # "style_external_links": False, + "style_nav_header_background": "#29BAF4", + # "collapse_navigation": True, + # "sticky_navigation": True, + # "navigation_depth": 4, + # "includehidden": True, + # "titles_only": False, +} + +# -- apidoc --------------------------------------------------- + +apidoc_module_dir = "../.." +apidoc_output_dir = "./generated" +apidoc_excluded_paths = exclude_patterns +apidoc_separate_modules = True +apidoc_toc_file = False +apidoc_module_first = False + + +# -- autodoc ----------------------------------------------------- + +autoclass_content = "class" +autodoc_member_order = "bysource" +autodoc_default_flags = ["members"] + + +# -- napoleon -------------------------------------------- + +# Parse Google style docstrings. +# See http://google-styleguide.googlecode.com/svn/trunk/pyguide.html +napoleon_google_docstring = True + +# Parse NumPy style docstrings. +# See https://github.com/numpy/numpy/blob/master/doc/HOWTO_DOCUMENT.rst.txt +napoleon_numpy_docstring = True + +# Should special members (like __membername__) and private members +# (like _membername) members be included in the documentation if they +# have docstrings. +napoleon_include_private_with_doc = False +napoleon_include_special_with_doc = True + +# If True, docstring sections will use the ".. admonition::" directive. +# If False, docstring sections will use the ".. rubric::" directive. +# One may look better than the other depending on what HTML theme is used. +napoleon_use_admonition_for_examples = False +napoleon_use_admonition_for_notes = False +napoleon_use_admonition_for_references = False + +# If True, use Sphinx :ivar: directive for instance variables: +# :ivar attr1: Description of attr1. +# :type attr1: type +# If False, use Sphinx .. attribute:: directive for instance variables: +# .. attribute:: attr1 +# +# Description of attr1. +# +# :type: type +napoleon_use_ivar = False + +# If True, use Sphinx :param: directive for function parameters: +# :param arg1: Description of arg1. +# :type arg1: type +# If False, output function parameters using the :parameters: field: +# :parameters: **arg1** (*type*) -- Description of arg1. +napoleon_use_param = False + +# If True, use Sphinx :rtype: directive for the return type: +# :returns: Description of return value. +# :rtype: type +# If False, output the return type inline with the return description: +# :returns: *type* -- Description of return value. +napoleon_use_rtype = False + + +# -- autosectionlabel -------------------------------------------- + +# Prefix document path to section labels, otherwise autogenerated labels would look like +# 'heading' rather than 'path/to/file:heading' +autosectionlabel_prefix_document = True diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 0000000..976932a --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,14 @@ +Content +======= + +.. toctree:: + :maxdepth: 4 + + generated/airstorm + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/gitmodel/__info__.py b/gitmodel/__info__.py new file mode 100644 index 0000000..6724426 --- /dev/null +++ b/gitmodel/__info__.py @@ -0,0 +1,5 @@ +__author__ = "Ben Davis" +__copyright__ = "2012, Ben Davis" +__email__ = "" +__license__ = "BSD-3-Clause" +__version__ = "0.1.0.dev1" diff --git a/gitmodel/__init__.py b/gitmodel/__init__.py index 8b13789..2705c17 100644 --- a/gitmodel/__init__.py +++ b/gitmodel/__init__.py @@ -1 +1 @@ - +from .__info__ import __version__, __copyright__, __email__, __author__ diff --git a/gitmodel/fields.py b/gitmodel/fields.py index 9bad1b4..09a070c 100644 --- a/gitmodel/fields.py +++ b/gitmodel/fields.py @@ -3,9 +3,10 @@ import os import re import uuid +import urllib + from datetime import datetime, date, time -from StringIO import StringIO -from urlparse import urlparse +from io import StringIO import pygit2 @@ -20,7 +21,7 @@ def __str__(self): return "No default provided." -class Field(object): +class Field: """The base implementation of a field used by a GitModel class.""" creation_counter = 0 @@ -76,6 +77,7 @@ def contribute_to_class(self, cls, name): if field.model: field = copy.copy(field) field.model = cls + print(cls._meta) cls._meta.add_field(field) def has_default(self): @@ -95,9 +97,9 @@ def empty(self, value): """Returns True if value is considered an empty value for this field""" return value is None or value == self.empty_value - def __cmp__(self, other): - # This is needed because bisect does not take a comparison function. - return cmp(self.creation_counter, other.creation_counter) + def __lt__(self, other): + # This is needed when we bisect the field in models.GitModelOptions.add_field. + return self.creation_counter < other.creation_counter def to_python(self, value): """ @@ -176,7 +178,7 @@ def to_python(self, value): if value is None: return None - return unicode(value) + return value class SlugField(CharField): @@ -232,7 +234,7 @@ def validate(self, value, model_instance): super(URLField, self).validate(value, model_instance) if self.empty(value): return - parsed = urlparse(value) + parsed = urllib.parse.urlparse(value) if not all((parsed.scheme, parsed.hostname)): raise ValidationError("invalid_url", self) if self.schemes and parsed.scheme.lower() not in self.schemes: @@ -240,7 +242,7 @@ def validate(self, value, model_instance): raise ValidationError("invalid_scheme", self, schemes=schemes) -class BlobFieldDescriptor(object): +class BlobFieldDescriptor: def __init__(self, field): self.field = field self.data = None @@ -295,7 +297,7 @@ def post_save(self, value, instance, commit=False): def get_data_path(self, instance): path = os.path.dirname(instance.get_data_path()) - path = os.path.join(path, self.name) + path = '/'.join([path, self.name]) return "{0}.data".format(path) def contribute_to_class(self, cls, name): @@ -395,7 +397,7 @@ def to_python(self, value): if isinstance(value, date): return value - if isinstance(value, basestring): + if isinstance(value, str): try: return isodate.parse_iso_date(value) except isodate.InvalidFormat: @@ -418,7 +420,7 @@ def to_python(self, value): if isinstance(value, date): return datetime(value.year, value.month, value.day) - if isinstance(value, basestring): + if isinstance(value, str): try: return isodate.parse_iso_datetime(value) except isodate.InvalidFormat: @@ -443,7 +445,7 @@ def to_python(self, value): if isinstance(value, time): return value - if isinstance(value, basestring): + if isinstance(value, str): try: return isodate.parse_iso_time(value) except isodate.InvalidFormat: @@ -452,7 +454,7 @@ def to_python(self, value): raise ValidationError("invalid", self) -class RelatedFieldDescriptor(object): +class RelatedFieldDescriptor: def __init__(self, field): self.field = field self.id = None @@ -482,7 +484,7 @@ def to_model(self): return self._to_model # if to_model is a string, it must be registered on the same workspace - if isinstance(self._to_model, basestring): + if isinstance(self._to_model, str): if not self.workspace.models.get(self._to_model): msg = "Could not find model '{0}'".format(self._to_model) raise FieldError(msg) @@ -518,7 +520,7 @@ def contribute_to_class(self, cls, name): setattr(cls, name, RelatedFieldDescriptor(self)) -class GitObjectFieldDescriptor(object): +class GitObjectFieldDescriptor: def __init__(self, field): self.field = field self.oid = None @@ -563,7 +565,7 @@ def __init__(self, **kwargs): super(GitObjectField, self).__init__(**kwargs) def to_python(self, value): - if not isinstance(value, (basestring, pygit2.Oid, pygit2.Object)): + if not isinstance(value, (str, pygit2.Oid, pygit2.Object)): raise ValidationError("invalid_object", self) if isinstance(value, pygit2.Oid): return value.hex @@ -603,5 +605,5 @@ def to_python(self, value): return value try: return json.loads(value) - except ValueError, e: + except ValueError as e: raise ValidationError(e) diff --git a/gitmodel/models.py b/gitmodel/models.py index c5a7176..72b22b9 100644 --- a/gitmodel/models.py +++ b/gitmodel/models.py @@ -10,7 +10,7 @@ from gitmodel import utils -class GitModelOptions(object): +class GitModelOptions: """ An options class for ``GitModel``. """ @@ -74,7 +74,7 @@ def get_data_path(self, object_id): passes the instance id. """ model_name = self.model_name.lower() - return os.path.join(model_name, unicode(object_id), self.data_filename) + return '/'.join([model_name, object_id, self.data_filename]) def add_field(self, field): """Insert a field into the fields list in correct order""" @@ -150,14 +150,12 @@ def _prepare(self, model): class DeclarativeMetaclass(type): def __new__(cls, name, bases, attrs): - super_new = super(DeclarativeMetaclass, cls).__new__ - parents = [b for b in bases if isinstance(b, DeclarativeMetaclass)] parents.reverse() if not parents: # Don't do anything special for the base GitModel - return super_new(cls, name, bases, attrs) + return type.__new__(cls, name, bases, attrs) # workspace that will be passed to GitModelOptions workspace = attrs.pop("__workspace__", None) @@ -169,13 +167,13 @@ def __new__(cls, name, bases, attrs): # don't do anything special for GitModels without a workspace if not workspace: - return super_new(cls, name, bases, attrs) + return type.__new__(cls, name, bases, attrs) # Create the new class, while leaving out the declared attributes # which will be added later module = attrs.pop("__module__") options_cls = attrs.pop("__optclass__", None) - new_class = super_new(cls, name, bases, {"__module__": module}) + new_class = type.__new__(cls, name, bases, {"__module__": module}) # grab the declared Meta meta = attrs.pop("Meta", None) @@ -295,8 +293,7 @@ def concrete(func, self, *args, **kwargs): return func(self, *args, **kwargs) -class GitModel(object): - __metaclass__ = DeclarativeMetaclass +class GitModel(metaclass=DeclarativeMetaclass): __workspace__ = None @concrete @@ -341,15 +338,9 @@ def __init__(self, **kwargs): super(GitModel, self).__init__() def __repr__(self): - try: - u = unicode(self) - except (UnicodeEncodeError, UnicodeDecodeError): - u = "[Bad Unicode Data]" return u"<{0}: {1}>".format(self._meta.model_name, u) def __str__(self): - if hasattr(self, "__unicode__"): - return unicode(self).encode("utf-8") return "{0} object".format(self._meta.model_name) def save(self, commit=False, **commit_info): @@ -408,7 +399,7 @@ def get_id(self): return getattr(self, self._meta.id_attr) def get_data_path(self): - id = unicode(self.get_id()) + id = self.get_id() return self._meta.get_data_path(id) @property diff --git a/gitmodel/serializers/json.py b/gitmodel/serializers/json.py index b570ef6..4553bb2 100644 --- a/gitmodel/serializers/json.py +++ b/gitmodel/serializers/json.py @@ -5,7 +5,7 @@ import datetime import decimal -from StringIO import StringIO +from io import StringIO from gitmodel.utils import json from gitmodel.serializers import python diff --git a/gitmodel/test/__init__.py b/gitmodel/test/__init__.py deleted file mode 100644 index 6ea9d84..0000000 --- a/gitmodel/test/__init__.py +++ /dev/null @@ -1,111 +0,0 @@ -import unittest -import inspect -import tempfile -import os -import re -import shutil -import pygit2 - - -class GitModelTestCase(unittest.TestCase): - """Sets up a temporary git repository for each test""" - - def setUp(self): - # For tests, it's easier to use global_config so that we don't - # have to pass a config object around. - from gitmodel.workspace import Workspace - from gitmodel import exceptions - from gitmodel import utils - - self.exceptions = exceptions - self.utils = utils - - # Create temporary repo to work from - self.repo_path = tempfile.mkdtemp(prefix="python-gitmodel-") - pygit2.init_repository(self.repo_path, False) - self.workspace = Workspace(self.repo_path) - - def tearDown(self): - # clean up test repo - shutil.rmtree(self.repo_path) - - -def get_module_suite(mod): - """ - Test modules may provide a suite() function, otherwise all TestCase - subclasses are gethered automatically into a TestSuite - """ - # modules may provide a suite() function, - if hasattr(mod, "suite"): - return mod.suite() - else: - # gather all testcases in this module into a suite - suite = unittest.TestSuite() - for name in dir(mod): - obj = getattr(mod, name) - if inspect.isclass(obj) and issubclass(obj, unittest.TestCase): - suite.addTest(unittest.makeSuite(obj)) - # Set a name attribute so we can find it later - if mod.__name__.endswith("tests"): - name = mod.__name__.split(".")[-2] - else: - name = mod.__name__.split(".")[-1] - name = re.sub(r"^test_", "", name) - suite.name = name - suite.module = mod - return suite - - -def get_all_suites(): - """Yields all testsuites""" - # Tests can be one of: - # - test/suitename/tests.py - # - test/test_suitename.py - test_dir = os.path.dirname(__file__) - for f in os.listdir(test_dir): - mod = None - if os.path.exists(os.path.join(test_dir, f, "tests.py")): - p = __import__( - "gitmodel.test.{}".format(f), globals(), locals(), ["tests"], -1 - ) - mod = p.tests - elif re.match(r"^test_\w+.py$", f): - modname = f.replace(".py", "") - p = __import__("gitmodel.test", globals(), locals(), [modname], -1) - mod = getattr(p, modname) - if mod: - suite = get_module_suite(mod) - yield suite - - -def default_suite(): - """Sets up the default test suite""" - suite = unittest.TestSuite() - for other_suite in get_all_suites(): - suite.addTest(other_suite) - return suite - - -class TestLoader(unittest.TestLoader): - """Allows tests to be referenced by name""" - - def loadTestsFromName(self, name, module=None): - if name == "suite": - return default_suite() - - testcase = None - if "." in name: - name, testcase = name.split(".", 1) - - for suite in get_all_suites(): - if suite.name == name: - if testcase is None: - return suite - return super(TestLoader, self).loadTestsFromName(testcase, suite.module) - - raise LookupError('could not find test case for "{}"'.format(name)) - - -def main(): - """Runs the default test suite as a command line application.""" - unittest.main(__name__, testLoader=TestLoader(), defaultTest="suite") diff --git a/gitmodel/utils/__init__.py b/gitmodel/utils/__init__.py index b63a2b0..c453e48 100644 --- a/gitmodel/utils/__init__.py +++ b/gitmodel/utils/__init__.py @@ -44,7 +44,7 @@ def make_signature(name, email, timestamp=None, offset=None, default_offset=None elif offset is None: offset = default_offset - return pygit2.Signature(name, email, timestamp, offset) + return pygit2.Signature(name, email, int(round(timestamp)), int(round(offset))) def treeish_to_tree(repo, obj): diff --git a/gitmodel/utils/dict.py b/gitmodel/utils/dict.py index a599ade..89be8ed 100644 --- a/gitmodel/utils/dict.py +++ b/gitmodel/utils/dict.py @@ -7,6 +7,6 @@ def dict_strip_unicode_keys(uni_dict): data = {} for key, value in uni_dict.items(): - data[str(key)] = value + data[key] = value return data diff --git a/gitmodel/utils/encoding.py b/gitmodel/utils/encoding.py index c1cc150..e69de29 100644 --- a/gitmodel/utils/encoding.py +++ b/gitmodel/utils/encoding.py @@ -1,97 +0,0 @@ -""" -Taken from Django's utls.encoding and modified -""" -import datetime -import types -from decimal import Decimal - - -class GitModelUnicodeDecodeError(UnicodeDecodeError): - def __init__(self, obj, *args): - self.obj = obj - UnicodeDecodeError.__init__(self, *args) - - def __str__(self): - original = UnicodeDecodeError.__str__(self) - msg = "{0}. You passed in {1!r} ({2})" - return msg.format(original, self.obj, type(self.obj)) - - -def is_protected_type(obj): - """Determine if the object instance is of a protected type. - - Objects of protected types are preserved as-is when passed to - force_unicode(strings_only=True). - """ - return isinstance( - obj, - ( - types.NoneType, - int, - long, - datetime.datetime, - datetime.date, - datetime.time, - float, - Decimal, - ), - ) - - -def force_unicode(s, encoding="utf-8", strings_only=False, errors="strict"): - """ - Similar to smart_unicode, except that lazy instances are resolved to - strings, rather than kept as lazy objects. - - If strings_only is True, don't convert (some) non-string-like objects. - """ - # Handle the common case first, saves 30-40% in performance when s - # is an instance of unicode. This function gets called often in that - # setting. - if isinstance(s, unicode): - return s - if strings_only and is_protected_type(s): - return s - try: - if not isinstance( - s, - basestring, - ): - if hasattr(s, "__unicode__"): - s = unicode(s) - else: - try: - s = unicode(str(s), encoding, errors) - except UnicodeEncodeError: - if not isinstance(s, Exception): - raise - # If we get to here, the caller has passed in an Exception - # subclass populated with non-ASCII data without special - # handling to display as a string. We need to handle this - # without raising a further exception. We do an - # approximation to what the Exception's standard str() - # output should be. - s = u" ".join( - [ - force_unicode(arg, encoding, strings_only, errors) - for arg in s - ] - ) - elif not isinstance(s, unicode): - # Note: We use .decode() here, instead of unicode(s, encoding, - # errors), so that if s is a SafeString, it ends up being a - # SafeUnicode at the end. - s = s.decode(encoding, errors) - except UnicodeDecodeError, e: - if not isinstance(s, Exception): - raise GitModelUnicodeDecodeError(s, *e.args) - else: - # If we get to here, the caller has passed in an Exception - # subclass populated with non-ASCII bytestring data without a - # working unicode method. Try to handle this without raising a - # further exception by individually forcing the exception args - # to unicode. - s = u" ".join( - [force_unicode(arg, encoding, strings_only, errors) for arg in s] - ) - return s diff --git a/gitmodel/utils/path.py b/gitmodel/utils/path.py index 4a1ecf0..cfc3057 100644 --- a/gitmodel/utils/path.py +++ b/gitmodel/utils/path.py @@ -25,7 +25,7 @@ def build_path(repo, path, entries=None, root=None): The root tree OID is returned, so that it can be included in a commit or stage. """ - path = path.strip(os.path.sep) + path = path.strip("/") if path is not None and path != "": parent, name = os.path.split(path) else: @@ -36,7 +36,7 @@ def build_path(repo, path, entries=None, root=None): root_id = repo.TreeBuilder().write() root = repo[root_id] - if isinstance(root, (basestring, pygit2.Oid)): + if isinstance(root, (str, pygit2.Oid)): root = repo[root] if parent is None: @@ -125,7 +125,7 @@ def glob(repo, tree, pathname): glob_in_dir = glob0 for dirname in dirs: for name in glob_in_dir(repo, tree, dirname, basename): - yield os.path.join(dirname, name) + yield '/'.join([dirname, name]) # These 2 helper functions non-recursively glob inside a literal directory. @@ -136,8 +136,8 @@ def glob(repo, tree, pathname): def glob1(repo, tree, dirname, pattern): if not dirname: dirname = os.curdir - if isinstance(pattern, unicode) and not isinstance(dirname, unicode): - dirname = unicode( + if isinstance(pattern, str) and not isinstance(dirname, str): + dirname = str( dirname, sys.getfilesystemencoding() or sys.getdefaultencoding() ) if dirname != os.curdir: @@ -160,7 +160,7 @@ def glob0(repo, tree, dirname, basename): if repo[entry.oid].type == pygit2.GIT_OBJ_TREE: return [basename] else: - if path_exists(tree, os.path.join(dirname, basename)): + if path_exists(tree, '/'.join([dirname, basename])): return [basename] return [] diff --git a/gitmodel/workspace.py b/gitmodel/workspace.py index 317ceb5..d1d59f1 100644 --- a/gitmodel/workspace.py +++ b/gitmodel/workspace.py @@ -12,7 +12,7 @@ from gitmodel import utils -class Workspace(object): +class Workspace: """ A workspace acts as an encapsulation within which any model work is done. It is analogous to a git working directory. It also acts as a "porcelain" @@ -25,7 +25,7 @@ class Workspace(object): Passing initial_branch will set the default head for the workspace. """ - def __init__(self, repo_path, initial_branch="refs/heads/master"): + def __init__(self, repo_path, initial_branch="refs/heads/main"): self.config = conf.Config() # set up a model registry @@ -125,7 +125,7 @@ def import_models(self, path_or_module): """ Register all models declared within a given python module """ - if isinstance(path_or_module, basestring): + if isinstance(path_or_module, str): mod = import_module(path_or_module) else: mod = path_or_module @@ -359,7 +359,7 @@ def sync_repo_index(self, checkout=True): self.repo.checkout() -class Branch(object): +class Branch: """ A representation of a git branch that provides quick access to the ref, commit, and commit tree. @@ -371,7 +371,7 @@ def __init__(self, repo, ref_name): except KeyError: msg = "Reference not found: {}".format(ref_name) raise exceptions.RepositoryError(msg) - self.commit = self.ref.get_object() + self.commit = self.ref.peel() self.oid = self.commit.oid self.tree = self.commit.tree diff --git a/pyrightconfig.json b/pyrightconfig.json new file mode 100644 index 0000000..bdf8635 --- /dev/null +++ b/pyrightconfig.json @@ -0,0 +1,5 @@ +{ + "venvPath": "$WORKON_HOME", + "venv": "gitmodel", + "reportGeneralTypeIssues": false +} diff --git a/python-gitmodel.sublime-project b/python-gitmodel.sublime-project new file mode 100644 index 0000000..b6a9fbc --- /dev/null +++ b/python-gitmodel.sublime-project @@ -0,0 +1,63 @@ +{ + "build_systems": [ + { + "cmd": [ + "pip", + "install", + "--editable", + ".[ci]" + ], + "name": "Install requirements with PyPI", + "path": "$WORKON_HOME/gitmodel/bin:$PATH", + "windows": { + "path": "$WORKON_HOME/gitmodel/Scripts;$PATH", + }, + "working_dir": "$project_path", + }, + { + "cmd": [ + "pytest", + "--cov-report=html", + "--cov=gitmodel" + ], + "name": "Test with pytest", + "path": "$WORKON_HOME/gitmodel/bin:$PATH", + "windows": { + "path": "$WORKON_HOME/gitmodel/Scripts;$PATH", + }, + "working_dir": "$project_path", + }, + { + "cmd": [ + "sphinx-build", + "./docs/source", + "./docs/build" + ], + "name": "Document with Sphinx", + "path": "$WORKON_HOME/gitmodel/bin:$PATH", + "windows": { + "path": "$WORKON_HOME/gitmodel/Scripts;$PATH", + }, + "working_dir": "$project_path", + } + ], + "folders": [ + { + "file_exclude_patterns": [ + ".coverage" + ], + "folder_exclude_patterns": [ + "__pycache__", + "htmlcov", + "*.egg-info", + ".pytest_cache", + "build" + ], + "path": ".", + }, + { + "path": "C:\\Users\\Douglas\\Desktop\\pygit2-master" + }, + ], + "virtualenv": "$WORKON_HOME/gitmodel", +} diff --git a/requirements-ci.txt b/requirements-ci.txt new file mode 100644 index 0000000..42628c6 --- /dev/null +++ b/requirements-ci.txt @@ -0,0 +1 @@ +.[ci] diff --git a/run-tests.py b/run-tests.py deleted file mode 100755 index 823a4cd..0000000 --- a/run-tests.py +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env python -import sys -import os - -sys.path.insert(0, os.path.abspath(os.path.dirname(__file__))) - -from gitmodel.test import main - -if __name__ == "__main__": - main() diff --git a/setup.py b/setup.py index 33ab834..fffaaf3 100644 --- a/setup.py +++ b/setup.py @@ -1,15 +1,46 @@ -from distutils.core import setup +"""Setup for gitarmony. +""" + +import os + +from setuptools import setup + +dirname = os.path.dirname(__file__) +info = {} +with open(os.path.join(dirname, "gitmodel", "__info__.py"), mode="r") as f: + exec(f.read(), info) # pylint: disable=W0122 + +# Get the long description from the README file. +with open(os.path.join(dirname, "README.md"), encoding="utf-8") as fle: + long_description = fle.read() setup( - name="python-gitmodel", - version="0.1dev", - test_suite="gitmodel.test", - packages=[ - "gitmodel", - "gitmodel.serializers", - "gitmodel.utils", - ], - install_requires=["pygit2", "python-dateutil", "decorator"], - license="Creative Commons Attribution-Noncommercial-Share Alike license", - long_description=open("README.rst").read(), + name="gitmodel", + version=info.get("__version__", ""), + description="A distributed, versioned data store for Python.", + long_description=long_description, + long_description_content_type="text/markdown", + url="https://github.com/bendavis78/python-gitmodel", + author=info.get("__author__", ""), + author_email=info.get("__email__", ""), + license=info.get("__license__", ""), + packages=["gitmodel"], + install_requires=["pygit2~=1.7", "python-dateutil~=2.8", "decorator~=5.1"], + extras_require={ + "ci": [ + "flake8-print~=3.1", + "flake8~=3.8", + "pep8-naming~=0.11", + "pytest-cov~=2.10.1", + "pytest-html~=2.1.1", + "pytest-pep8~=1.0.6", + "pytest~=6.1.1", + "requests-mock~=1.8", + "sphinx-markdown-tables~=0.0", + "sphinx-rtd-theme~=0.5", + "sphinxcontrib-apidoc~=0.3", + "Sphinx~=3.2", + ], + }, + include_package_data=True, ) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..c763186 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,36 @@ +import unittest +import inspect +import tempfile +import os +import re +import shutil +import pygit2 +import logging + + +class GitModelTestCase(unittest.TestCase): + """Sets up a temporary git repository for each test""" + + def setUp(self): + # For tests, it's easier to use global_config so that we don't + # have to pass a config object around. + from gitmodel.workspace import Workspace + from gitmodel import exceptions + from gitmodel import utils + + self.exceptions = exceptions + self.utils = utils + + # Create temporary repo to work from + self.repo_path = tempfile.mkdtemp(prefix="python-gitmodel-") + pygit2.init_repository(self.repo_path, False) + self.workspace = Workspace(self.repo_path) + + def tearDown(self): + # clean up test repo + try: + shutil.rmtree(self.repo_path) + except PermissionError as error: + logging.error(error) + + diff --git a/gitmodel/test/fields/__init__.py b/tests/fields/__init__.py similarity index 100% rename from gitmodel/test/fields/__init__.py rename to tests/fields/__init__.py diff --git a/gitmodel/test/fields/git-logo-2color.png b/tests/fields/git-logo-2color.png similarity index 100% rename from gitmodel/test/fields/git-logo-2color.png rename to tests/fields/git-logo-2color.png diff --git a/gitmodel/test/fields/models.py b/tests/fields/models.py similarity index 100% rename from gitmodel/test/fields/models.py rename to tests/fields/models.py diff --git a/gitmodel/test/model/__init__.py b/tests/model/__init__.py similarity index 100% rename from gitmodel/test/model/__init__.py rename to tests/model/__init__.py diff --git a/gitmodel/test/model/diff_branch.diff b/tests/model/diff_branch.diff similarity index 100% rename from gitmodel/test/model/diff_branch.diff rename to tests/model/diff_branch.diff diff --git a/gitmodel/test/model/diff_nobranch.diff b/tests/model/diff_nobranch.diff similarity index 100% rename from gitmodel/test/model/diff_nobranch.diff rename to tests/model/diff_nobranch.diff diff --git a/gitmodel/test/model/models.py b/tests/model/models.py similarity index 95% rename from gitmodel/test/model/models.py rename to tests/model/models.py index c581493..75f1745 100644 --- a/gitmodel/test/model/models.py +++ b/tests/model/models.py @@ -36,7 +36,7 @@ def get_path_custom(opts, object_id): # kinda silly, but good for testing that the override works model_name = opts.model_name.lower() model_name = model_name.replace("alternate", "-alt") - return os.path.join(model_name, unicode(object_id), "data.json") + return '/'.join([model_name, object_id, "data.json"]) class PostAlternate(GitModel): diff --git a/gitmodel/test/model/tests.py b/tests/test_model.py similarity index 95% rename from gitmodel/test/model/tests.py rename to tests/test_model.py index cbc3693..4568936 100644 --- a/gitmodel/test/model/tests.py +++ b/tests/test_model.py @@ -1,16 +1,17 @@ import os import json -from gitmodel.test import GitModelTestCase +from . import GitModelTestCase +from .model import models +from .model.models import Author -class TestInstancesMixin(object): +from gitmodel import exceptions +from gitmodel import fields + +class TestInstancesMixin: def setUp(self): super(TestInstancesMixin, self).setUp() - from gitmodel.test.model import models - from gitmodel import exceptions - from gitmodel import fields - self.exceptions = exceptions self.fields = fields self.workspace.import_models(models) @@ -99,7 +100,7 @@ def test_save(self): # verify data data = json.loads(blob.data) - self.assertItemsEqual( + self.assertCountEqual( data, { "model": "Author", @@ -146,7 +147,7 @@ def test_save_commit(self): # verify data data = json.loads(blob.data) - self.assertItemsEqual( + self.assertCountEqual( data, { "model": "Author", @@ -167,7 +168,7 @@ def test_diff_nobranch(self): self.assertTrue(self.workspace.has_changes()) blob_hash = self.workspace.index[self.author.get_data_path()].hex[:7] diff = open( - os.path.join(os.path.dirname(__file__), "diff_nobranch.diff") + os.path.join(os.path.dirname(__file__), "model", "diff_nobranch.diff") ).read() diff = diff.format(self.author.get_data_path(), blob_hash, self.author.id) self.assertMultiLineEqual(diff, self.workspace.diff().patch) @@ -180,14 +181,14 @@ def test_diff_branch(self): self.author.first_name = "Jane" self.author.save() blob_hash_2 = self.workspace.index[self.author.get_data_path()].hex[:7] - diff = open(os.path.join(os.path.dirname(__file__), "diff_branch.diff")).read() + diff = open(os.path.join(os.path.dirname(__file__), "model", "diff_branch.diff")).read() diff = diff.format( self.author.get_data_path(), blob_hash_1, blob_hash_2, self.author.id ) self.assertMultiLineEqual(diff, self.workspace.diff().patch) def test_save_commit_history(self): - # Test that commited models save correctly + # Test that committed models save correctly import pygit2 commit1 = self.author.save(commit=True, message="Test first commit") @@ -239,7 +240,7 @@ def test_custom_id_attr(self): self.assertEqual(self.post.get_id(), self.post.slug) def test_overridden_id_field(self): - # tests bug that occured when overriding the id field and not using + # tests bug that occurred when overriding the id field and not using # that field as the id_attr class Resource(self.models.GitModel): __workspace__ = self.workspace @@ -298,7 +299,6 @@ def test_multiple_saves_before_commit(self): def test_concrete(self): # import the unregistered model - from gitmodel.test.model.models import Author self.author.save() id = self.author.get_id() diff --git a/gitmodel/test/test_utils.py b/tests/test_utils.py similarity index 87% rename from gitmodel/test/test_utils.py rename to tests/test_utils.py index 907ada4..d6e2060 100644 --- a/gitmodel/test/test_utils.py +++ b/tests/test_utils.py @@ -1,10 +1,18 @@ +from time import time +from datetime import datetime + import pygit2 -from gitmodel.test import GitModelTestCase + +from dateutil.tz import tzlocal + +from gitmodel import utils + +from . import GitModelTestCase class GitModelUtilsTest(GitModelTestCase): def setUp(self): - super(GitModelUtilsTest, self).setUp() + GitModelTestCase.setUp(self) self.repo = self.workspace.repo def _get_test_tree(self): @@ -37,8 +45,6 @@ def _get_test_tree(self): return root def test_describe_tree(self): - from gitmodel import utils - root = self._get_test_tree() desc = utils.path.describe_tree(self.repo, root) test_desc = ( @@ -52,10 +58,6 @@ def test_describe_tree(self): self.assertMultiLineEqual(desc, test_desc) def test_make_signature(self): - from gitmodel import utils - from datetime import datetime - from time import time - from dateutil.tz import tzlocal # Get local offset timestamp = time() @@ -88,9 +90,6 @@ def test_make_signature(self): self.assertAlmostEqual(test_sig.time, timestamp, delta=10) def test_build_path_empty(self): - # Test building a path from an empty tree - from gitmodel import utils - path = "/foo/bar/baz/" # path sep should be stripped # create dummy entry blob_oid = self.repo.create_blob("TEST CONTENT") @@ -101,9 +100,6 @@ def test_build_path_empty(self): self.assertMultiLineEqual(desc, test_desc) def test_build_path_update(self): - # Test building a path from an existing tree, updating the path - from gitmodel import utils - path = "/foo/bar/baz/" # path sep should be stripped # build initial tree blob_oid = self.repo.create_blob("TEST CONTENT") @@ -119,13 +115,11 @@ def test_build_path_update(self): new_content = self.repo[entry.oid].data desc = utils.path.describe_tree(self.repo, tree2) test_desc = "foo/\n bar/\n baz/\n qux.txt" - self.assertEqual(new_content, "UPDATED CONTENT") + self.assertEqual(new_content, b"UPDATED CONTENT") self.assertMultiLineEqual(desc, test_desc) def test_glob(self): - from gitmodel import utils - tree = self._get_test_tree() files = utils.path.glob(self.repo, tree, "foo/*/*.txt") test = ["foo/bar/test.txt", "foo/bar/test3.txt"] - self.assertEqual(list(files), test) + self.assertEqual(test, list(files)) diff --git a/gitmodel/test/test_workspace.py b/tests/test_workspace.py similarity index 80% rename from gitmodel/test/test_workspace.py rename to tests/test_workspace.py index 9584f07..edb0c42 100644 --- a/gitmodel/test/test_workspace.py +++ b/tests/test_workspace.py @@ -1,6 +1,7 @@ -from gitmodel.test import GitModelTestCase from gitmodel import exceptions +from . import GitModelTestCase + class GitModelWorkspaceTest(GitModelTestCase): def setUp(self): @@ -24,14 +25,25 @@ def test_register_model(self): from gitmodel.models import GitModel, DeclarativeMetaclass from gitmodel import fields - class TestModel(GitModel): + class TestModelA(GitModel): + foo = fields.CharField() + bar = fields.CharField() + + self.workspace.register_model(TestModelA) + + self.assertIsNotNone(self.workspace.models.get("TestModelA")) + test_model = self.workspace.models.TestModelA() + self.assertIsInstance(test_model, self.workspace.models.TestModelA) + self.assertIsInstance(type(test_model), DeclarativeMetaclass) + self.assertEqual(test_model._meta.workspace, self.workspace) + + class TestModelB(self.workspace.GitModel): foo = fields.CharField() bar = fields.CharField() - self.workspace.register_model(TestModel) - self.assertIsNotNone(self.workspace.models.get("TestModel")) - test_model = self.workspace.models.TestModel() - self.assertIsInstance(test_model, self.workspace.models.TestModel) + self.assertIsNotNone(self.workspace.models.get("TestModelB")) + test_model = self.workspace.models.TestModelB() + self.assertIsInstance(test_model, self.workspace.models.TestModelB) self.assertIsInstance(type(test_model), DeclarativeMetaclass) self.assertEqual(test_model._meta.workspace, self.workspace) @@ -43,24 +55,24 @@ def test_init_existing_branch(self): self.workspace.add_blob("test.txt", "Test") self.workspace.commit("initial commit") new_workspace = Workspace(self.workspace.repo.path) - self.assertEqual(new_workspace.branch.ref.name, "refs/heads/master") + self.assertEqual(new_workspace.branch.ref.name, "refs/heads/main") self.assertEqual(new_workspace.branch.commit.message, "initial commit") def test_getitem(self): self.workspace.add_blob("test.txt", "Test") entry = self.workspace.index["test.txt"] - self.assertEqual(self.repo[entry.oid].data, "Test") + self.assertEqual(b"Test", self.repo[entry.oid].data) def test_branch_property(self): self.assertIsNone(self.workspace.branch) self.workspace.add_blob("test.txt", "Test") self.workspace.commit("initial commit") self.assertIsNotNone(self.workspace.branch) - self.assertEqual(self.workspace.branch.ref.name, "refs/heads/master") + self.assertEqual(self.workspace.branch.ref.name, "refs/heads/main") self.assertEqual(self.workspace.branch.commit.message, "initial commit") def test_set_branch(self): - # create intial master branch + # create intial main branch self.workspace.add_blob("test.txt", "Test") self.workspace.commit("initial commit") # create a new branch @@ -72,12 +84,12 @@ def test_set_branch(self): entry = self.workspace.index["test.txt"] test_content = self.repo[entry.oid].data - self.assertEqual(test_content, "Test 2") + self.assertEqual(b"Test 2", test_content) - self.workspace.set_branch("master") + self.workspace.set_branch("main") entry = self.workspace.index["test.txt"] test_content = self.repo[entry.oid].data - self.assertEqual(test_content, "Test") + self.assertEqual(b"Test", test_content) def test_set_nonexistant_branch(self): with self.assertRaises(KeyError): @@ -94,12 +106,12 @@ def test_update_index_with_pending_changes(self): def test_add_blob(self): self.workspace.add_blob("test.txt", "Test") entry = self.workspace.index["test.txt"] - self.assertEqual(self.repo[entry.oid].data, "Test") + self.assertEqual(b"Test", self.repo[entry.oid].data) def test_remove(self): self.workspace.add_blob("test.txt", "Test") entry = self.workspace.index["test.txt"] - self.assertEqual(self.repo[entry.oid].data, "Test") + self.assertEqual(b"Test", self.repo[entry.oid].data) self.workspace.remove("test.txt") with self.assertRaises(KeyError): self.workspace.index["test.txt"] @@ -121,14 +133,14 @@ class TestException(Exception): except TestException: pass # since commit should have failed, current branch should be nonexistent - self.assertEqual(self.workspace.branch, None) + self.assertEqual(None, self.workspace.branch) def test_commit_on_success_with_pending_changes(self): self.workspace.add_blob("foo.txt", "Foobar") with self.assertRaisesRegexp(exceptions.RepositoryError, r"pending"): with self.workspace.commit_on_success("Test commit"): self.workspace.add_blob("test.txt", "Test") - self.assertEqual(self.workspace.branch, None) + self.assertEqual(None, self.workspace.branch) def test_has_changes(self): self.workspace.add_blob("foo.txt", "Foobar") diff --git a/gitmodel/test/fields/tests.py b/tests/tests.py similarity index 96% rename from gitmodel/test/fields/tests.py rename to tests/tests.py index a7fb5b8..0bc6ecf 100644 --- a/gitmodel/test/fields/tests.py +++ b/tests/tests.py @@ -1,15 +1,18 @@ import os +from datetime import time, datetime +from dateutil import tz import pygit2 -from gitmodel.test import GitModelTestCase +from .fields import models +from . import GitModelTestCase class TestInstancesMixin(object): def setUp(self): super(TestInstancesMixin, self).setUp() - from gitmodel.test.fields import models + self.models = self.workspace.import_models(models) @@ -105,7 +108,7 @@ def test_validate_time(self): class FieldTypeCheckingTest(TestInstancesMixin, GitModelTestCase): def assertTypesMatch(self, field, test_values, type): - for value, eq_value in test_values.iteritems(): + for value, eq_value in iter(test_values.items()): setattr(self.person, field, value) self.person.save() person = self.models.Person.get(self.person.id) @@ -113,14 +116,12 @@ def assertTypesMatch(self, field, test_values, type): self.assertEqual(getattr(person, field), eq_value) def test_char(self): - from datetime import datetime - test_values = { "John": "John", 0.007: "0.007", datetime(2012, 12, 12): "2012-12-12 00:00:00", } - self.assertTypesMatch("first_name", test_values, basestring) + self.assertTypesMatch("first_name", test_values, str) def test_integer(self): test_values = {33: 33, "33": 33} @@ -128,7 +129,7 @@ def test_integer(self): def test_float(self): test_values = {0.825: 0.825, "0.825": 0.825} - self.assertTypesMatch("tax_rate", test_values, float) + self.assertTypesMatch("tax_rate", test_values, str) def test_decimal(self): from decimal import Decimal @@ -174,9 +175,6 @@ def test_datetime(self): self.assertEqual(person.date_joined, datetime(2012, 1, 1, 0, 0)) def test_time(self): - from datetime import time - from dateutil import tz - utc = tz.tzutc() utc_offset = tz.tzoffset(None, -1 * 4 * 60 * 60) test_values = {