diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..f2b382c --- /dev/null +++ b/.coveragerc @@ -0,0 +1,3 @@ +[report] +exclude_lines = + raise NotImplementedError diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..465aa34 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,9 @@ +language: python +python: + - "3.6" +install: + - pip install coveralls +script: + - coverage run --source=galactic setup.py test +after_success: + - coveralls diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..50207b0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2018, The Galactic Organization +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..b42efab --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,4 @@ +include README.md +include LICENSE +include MANIFEST.in + diff --git a/README.md b/README.md new file mode 100644 index 0000000..12e0a61 --- /dev/null +++ b/README.md @@ -0,0 +1,14 @@ +# py-galactic +[![Build Status](https://img.shields.io/travis/thegalactic/py-galactic/master.svg)](https://travis-ci.org/thegalactic/py-galactic/branches) +[![Coveralls](https://img.shields.io/coveralls/github/thegalactic/py-galactic/master.svg)](https://coveralls.io/github/thegalactic/py-galactic?branch=master) +[![Scrutinizer](https://img.shields.io/scrutinizer/g/thegalactic/py-galactic.svg)](https://scrutinizer-ci.com/g/thegalactic/py-galactic/) +[![Docs](https://img.shields.io/readthedocs/py-galactic.svg)](https://readthedocs.org/projects/py-galactic/) +[![PyPI version](https://img.shields.io/pypi/v/py-galactic.svg)](https://pypi.org/project/py-galactic/) +[![PyPI format](https://img.shields.io/pypi/format/py-galactic.svg)](https://pypi.org/project/py-galactic/) +[![License](https://img.shields.io/pypi/l/py-galactic.svg)](https://raw.githubusercontent.com/thegalactic/py-galactic/master/LICENSE) +[![Downloads](https://img.shields.io/pypi/dm/py-galactic.svg)](https://pypi.org/project/py-galactic/) +[![Python version](https://img.shields.io/pypi/pyversions/py-galactic.svg)](https://pypi.org/project/py-galactic/) +[![Development Status](https://img.shields.io/pypi/status/py-galactic.svg)](https://pypi.org/project/py-galactic/) + +*py-galactic* is a package developed in Python for studying [Formal Concept Analysis](https://en.wikipedia.org/wiki/Formal_concept_analysis). + diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..5149bf2 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SPHINXPROJ = py-galactic +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) \ No newline at end of file diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..e04738b --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,36 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build +set SPHINXPROJ=py-galactic + +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% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +:end +popd diff --git a/docs/source/api.context.memory.rst b/docs/source/api.context.memory.rst new file mode 100644 index 0000000..46108ad --- /dev/null +++ b/docs/source/api.context.memory.rst @@ -0,0 +1,5 @@ +MemoryContext +============= + +.. automodule:: galactic.context.memory + :members: diff --git a/docs/source/api.context.mixins.rst b/docs/source/api.context.mixins.rst new file mode 100644 index 0000000..dff5f4a --- /dev/null +++ b/docs/source/api.context.mixins.rst @@ -0,0 +1,5 @@ +Mixins +====== + +.. automodule:: galactic.context.mixins + :members: diff --git a/docs/source/api.context.rst b/docs/source/api.context.rst new file mode 100644 index 0000000..b9b6847 --- /dev/null +++ b/docs/source/api.context.rst @@ -0,0 +1,12 @@ +Context +======= + +.. automodule:: galactic.context + :members: + +.. toctree:: + :hidden: + :maxdepth: 1 + + api.context.mixins.rst + api.context.memory.rst diff --git a/docs/source/api.description.rst b/docs/source/api.description.rst new file mode 100644 index 0000000..4663a83 --- /dev/null +++ b/docs/source/api.description.rst @@ -0,0 +1,5 @@ +Description +=========== + +.. automodule:: galactic.description + :members: diff --git a/docs/source/api.type.rst b/docs/source/api.type.rst new file mode 100644 index 0000000..8d2a367 --- /dev/null +++ b/docs/source/api.type.rst @@ -0,0 +1,5 @@ +Type +==== + +.. automodule:: galactic.type + :members: diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000..863f3d1 --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,213 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# py-galactic documentation build configuration file, created by +# sphinx-quickstart on Sun Jan 14 18:35:19 2018. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# 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('.')) + +import os +import sys +sys.path.insert(0, os.path.abspath('../..')) + +on_rtd = os.environ.get('READTHEDOCS', None) == 'True' + + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# 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.todo', + 'sphinx.ext.coverage', + 'sphinx.ext.mathjax', + 'sphinx.ext.viewcode', + 'sphinx.ext.intersphinx', + 'sphinx.ext.napoleon' +] + +# Napoleon settings +napoleon_google_docstring = True +napoleon_numpy_docstring = True +#napoleon_include_init_with_doc = False +napoleon_include_init_with_doc = True +napoleon_include_private_with_doc = False +napoleon_include_special_with_doc = False +#napoleon_use_admonition_for_examples = False +#napoleon_use_admonition_for_notes = False +#napoleon_use_admonition_for_references = False +napoleon_use_admonition_for_examples = True +napoleon_use_admonition_for_notes = True +napoleon_use_admonition_for_references = True +#napoleon_use_ivar = False +napoleon_use_ivar = True +napoleon_use_param = True +napoleon_use_rtype = True + +# intersphinx setting +intersphinx_mapping = {'python': ('http://docs.python.org/3', None)} + +autodoc_member_order = 'bysource' + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = 'py-galactic' +copyright = '2018, The Galactic Organization' +author = 'The Galactic Organization' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '0.1' +# The full version, including alpha/beta/rc tags. +release = '0.1.0a1' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = [] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = True + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +if not on_rtd: # only import and set the theme if we're building docs locally + import sphinx_rtd_theme + html_theme = 'sphinx_rtd_theme' + html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# 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 = [] + +# Custom sidebar templates, must be a dictionary that maps document names +# to template names. +# +# This is required for the alabaster theme +# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars +html_sidebars = { + '**': [ + 'relations.html', # needs 'show_related': True theme option to display + 'searchbox.html', + ] +} + +html_favicon = 'thegalactic.ico' + +# -- Options for HTMLHelp output ------------------------------------------ + +# Output file base name for HTML help builder. +htmlhelp_basename = 'py-galacticdoc' + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'py-galactic.tex', 'py-galactic Documentation', + 'The Galactic Organization', 'manual'), +] + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'py-galactic', 'py-galactic Documentation', + [author], 1) +] + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'py-galactic', 'py-galactic Documentation', + author, 'py-galactic', 'One line description of project.', + 'Miscellaneous'), +] + + + diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 0000000..8235714 --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,35 @@ +.. py-galactic documentation master file, created by + sphinx-quickstart on Sun Jan 14 18:35:19 2018. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + +=========== +py-galactic +=========== + +*py-galactic* is a package for studying `Formal Concept Analysis `_. + +.. toctree:: + :maxdepth: 1 + :caption: Getting started + + install.rst + +.. toctree:: + :hidden: + :maxdepth: 1 + :caption: Reference + + api.context.rst + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + diff --git a/docs/source/install.rst b/docs/source/install.rst new file mode 100644 index 0000000..a5adce6 --- /dev/null +++ b/docs/source/install.rst @@ -0,0 +1,36 @@ +Install py-galactic +=================== + +*py-galactic* requires `python 3`_, a programming language that comes pre-installed on linux and Mac OS X, and which is easily installed `on Windows`_. + +Install *py-galactic* using the bash command + +.. code-block:: bash + + pip install py-galactic + +To upgrade to the most recent release, use + +.. code-block:: bash + + pip install --upgrade py-galactic + +`pip` is a script that downloads and installs modules from the Python Package Index, PyPI_. It should come installed with your python distribution. If you are running linux, `pip` may be bundled separately. On a Debian-based system (including Ubuntu), you can install it using + +.. code-block:: bash + + apt-get update + apt-get install python-pip + +.. _python 3: http://www.python.org/ +.. _on Windows: https://www.python.org/downloads/windows +.. _PyPI: https://pypi.org + + +Getting Help +------------ + +If you have any difficulties with *py-galactic*, please feel welcome to `file an issue`_ on github so that we can help. + +.. _file an issue: https://github.com/thegalactic/py-galactic/issues + diff --git a/docs/source/thegalactic.ico b/docs/source/thegalactic.ico new file mode 100644 index 0000000..9828c59 Binary files /dev/null and b/docs/source/thegalactic.ico differ diff --git a/galactic/__init__.py b/galactic/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/galactic/context/__init__.py b/galactic/context/__init__.py new file mode 100644 index 0000000..6c3d63a --- /dev/null +++ b/galactic/context/__init__.py @@ -0,0 +1,933 @@ +# This Python file uses the following encoding: utf-8 +""" +The :mod:`galactic.context` package defines generic classes for using contexts: + +* :class:`Context`: a context is composed by a population and a model +* :class:`Population` a population is a container for individuals +* :class:`Model` a model is a container for attributes +* :class:`Individual` an individual has an identifier and values +* :class:`Attribute` an attribute has a name and a type +""" + +from abc import abstractmethod +from typing import Container, Union, Mapping, Iterator, TypeVar, Generic + +C = TypeVar('C', bound='Context') +""" +Generic subclass of the :class:`Context` class +""" + +P = TypeVar('P', bound='Population') +""" +Generic subclass of the :class:`Population` class +""" + +M = TypeVar('M', bound='Model') +""" +Generic subclass of the :class:`Model` class +""" + +X = TypeVar('X', bound='Individual') +""" +Generic subclass of the :class:`Individual` class +""" + +A = TypeVar('A', bound='Attribute') +""" +Generic subclass of the :class:`Attribute` class +""" + + +# pylint: disable=too-few-public-methods +class Context(Generic[M, P, X, A], Container[Union[X, A]]): + """ + A :class:`Context` handles a model and a population. + + It's possible to access to the context :attr:`population` attribute or to + the context :attr:`model` attribute. + + Example + ------- + + >>> from galactic.context.memory import MemoryContext + >>> context = MemoryContext({'mybool': bool, 'myint': int}, ['0', '1']) + >>> print(context.population) + ['0', '1'] + >>> print(context.model) + {'mybool': , 'myint': } + + It's possible to check if a context is not empty (both :attr:`model` and :attr:`population` + are not empty) using the python builtin :func:`bool` function. + + It's possible to get a readable representation of a context using the python builtin :func:`str` + function. + + Example + ------- + + >>> print(context) + {'population': ['0', '1'], 'model': {'mybool': , 'myint': }} + + Example + ------- + + >>> bool(context) + True + + Contexts are container for individuals and attributes. It's possible to know if an individual + or an attribute belongs to a context using the python :keyword:`in` keyword. + + Example + ------- + + >>> context.model['mybool'] in context + True + >>> context.population['0'] in context + True + + .. versionadded:: 0.0.1 + """ + + @property + @abstractmethod + def population(self) -> P: + """ + Get the population for this context. + + Returns + ------- + the underlying population : :class:`P` + + .. versionadded:: 0.0.1 + """ + raise NotImplementedError + + @property + @abstractmethod + def model(self) -> M: + """ + Get the underlying model. + + Returns + ------- + the underlying model : :class:`M` + + .. versionadded:: 0.0.1 + """ + raise NotImplementedError + + def __contains__(self, element: Union[X, A]): + """ + Check if an individual or an attribute is in this context. + + Parameters + ---------- + element : Union[:class:`X`, :class:`A`] + the element to check + + Returns + ------- + the membership of the element to the context : :class:`bool` + + .. versionadded:: 0.0.1 + """ + return element.context == self + + def __bool__(self): + """ + Check if this context is not empty. + + Returns + ------- + True if it contains some attribute and some individuals : :class:`bool` + + + .. versionadded:: 0.0.1 + """ + return bool(self.population) and bool(self.model) + + def __str__(self): + """ + Convert this context to a readable string. + + Returns + ------- + the user friendly readable string of this context : :class:`str` + + .. versionadded:: 0.0.1 + """ + return str({ + 'population': [identifier for identifier in self.population], + 'model': {name: attribute.type for name, attribute in self.model.items()} + }) + + +# pylint: disable=too-few-public-methods,function-redefined +class Population(Generic[C, M, X], Mapping[str, X]): + """ + A :class:`Population` is a container for individuals. + + It's possible to access to the population :attr:`context` attribute. + + Example + ------- + + >>> from galactic.context.memory import MemoryContext + >>> context = MemoryContext({'mybool': bool, 'myint': int}, ['0', '1']) + >>> population = context.population + >>> print(population.context) + {'population': ['0', '1'], 'model': {'mybool': , 'myint': }} + + It's possible to get a readable representation of a population. + + Example + ------- + + >>> print(population) + ['0', '1'] + + It's possible to check if a population is not empty using the python builtin :func:`bool` + function. + + Example + ------- + + >>> bool(population) + True + + It's possible to access to an individual with its identifier using the python array access + construct. + + Example + ------- + + >>> print(population['0']) + {'mybool': False, 'myint': 0} + + It's possible to check if an individual belongs to a population using the python + :keyword:`in` keyword. + + Example + ------- + + >>> '0' in population + True + + It's possible to iterate over a population using the python :keyword:`for` keyword. + + Example + ------- + + >>> {ident: str(individual) for ident, individual in population.items()} + {'0': "{'mybool': False, 'myint': 0}", '1': "{'mybool': False, 'myint': 0}"} + + It's possible to get the length of a population using the python builtin :func:`len` + function. + + Example + ------- + + >>> len(population) + 2 + + .. versionadded:: 0.0.1 + """ + + @property + @abstractmethod + def context(self) -> C: + """ + Get the underlying context. + + Returns + ------- + the underlying context : :class:`C` + + .. versionadded:: 0.0.1 + """ + raise NotImplementedError + + @property + def model(self) -> M: + """ + Get the underlying model. + + Returns + ------- + the underlying model : :class:`M` + + .. versionadded:: 0.0.1 + """ + return self.context.model + + @abstractmethod + def __bool__(self): + """ + Check if this population is not empty. + + Returns + ------- + True if this population is not empty : :class:`bool` + + .. versionadded:: 0.0.1 + """ + raise NotImplementedError + + def __str__(self): + """ + Convert this population to a readable string. + + Returns + ------- + the user friendly readable string of this population : :class:`str` + + .. versionadded:: 0.0.1 + """ + return str([identifier for identifier in self]) + + +class Model(Generic[C, P, A], + Mapping[str, A]): # pylint: disable=too-few-public-methods,function-redefined + """ + A :class:`Model` is a container for attributes. + + It's possible to access to the model :attr:`context` attribute. + + Example + ------- + + >>> from galactic.context.memory import MemoryContext + >>> context = MemoryContext({'mybool': bool, 'myint': int}, ['0', '1']) + >>> model = context.model + >>> print(model.context) + {'population': ['0', '1'], 'model': {'mybool': , 'myint': }} + + It's possible to get a readable representation of a model using the python builtin :func:`str` + function. + + Example + ------- + + >>> print(model) + {'mybool': , 'myint': } + + It's possible to check if a model is not empty using the python builtin :func:`bool` function. + + Example + ------- + + >>> bool(model) + True + + It's possible to access to an attribute with its name using the python array access + construct. + + Example + ------- + + >>> print(model['mybool']) + {'name': 'mybool', 'type': } + + It's possible to check if an attribute belongs to a model using the python + :keyword:`in` keyword. + + Example + ------- + + >>> 'mybool' in model + True + + It's possible to iterate over a model using the python :keyword:`for` keyword. + + Example + ------- + + >>> {name: attribute.type for name, attribute in model.items()} + {'mybool': , 'myint': } + + It's possible to get the length of a population using the python builtin :func:`len` + function. + + Example + ------- + + >>> len(model) + 2 + + .. versionadded:: 0.0.1 + """ + + @property + @abstractmethod + def context(self) -> C: + """ + Get the underlying context. + + Returns + ------- + the underlying context : :class:`C` + + .. versionadded:: 0.0.1 + """ + raise NotImplementedError + + @property + def population(self) -> P: + """ + Get the underlying population. + + Returns + ------- + the underlying population : :class:`P` + + .. versionadded:: 0.0.1 + """ + return self.context.population + + @abstractmethod + def __bool__(self): + """ + Check if this model is not empty. + + Returns + ------- + True if this model is not empty : :class:`bool` + + .. versionadded:: 0.0.1 + """ + raise NotImplementedError + + def __str__(self): + """ + Convert this model to a readable string. + + Returns + ------- + the user friendly readable string of this model : :class:`str` + + .. versionadded:: 0.0.1 + """ + return str({name: attribute.type for name, attribute in self.items()}) + + +# pylint: disable=too-few-public-methods,function-redefined +class Individual(Generic[C, P, M, X, A], Mapping[str, object]): + """ + A :class:`Individual` is a container for values. + + It's possible to access to the individual :attr:`identifier` attribute. + + Example + ------- + + >>> from galactic.context.memory import MemoryContext + >>> context = MemoryContext({'mybool': bool, 'myint': int}, ['0', '1']) + >>> individual = context.population['0'] + >>> individual.identifier + '0' + + It's possible to access to the individual :attr:`context` attribute. + + Example + ------- + + >>> print(individual.context) + {'population': ['0', '1'], 'model': {'mybool': , 'myint': }} + + It's possible to access to the individual :attr:`model` attribute. + + Example + ------- + + >>> print(individual.model) + {'mybool': , 'myint': } + + It's possible to access to the individual :attr:`population` attribute. + + Example + ------- + + >>> print(individual.population) + ['0', '1'] + + It's possible to get a readable representation of an individual using the python builtin + :func:`str` function. + + Example + ------- + + >>> print(individual) + {'mybool': False, 'myint': 0} + + It's possible to access to the individual values using the :meth:`value` method. + + Example + ------- + + >>> attribute = individual.model['mybool'] + >>> individual.value(attribute) + False + + It's possible to access to the individual values using the python array access + construct. + + Example + ------- + + >>> individual['mybool'] + False + + It's possible to get the length of an individual using the python builtin :func:`len` + function. + + Example + ------- + + >>> len(individual) + 2 + + It's possible to iterate over an individual using the python :keyword:`for` keyword. + + Example + ------- + + >>> {name: value for name, value in individual.items()} + {'mybool': False, 'myint': 0} + + .. versionadded:: 0.0.1 + """ + + @property + @abstractmethod + def identifier(self) -> str: + """ + Get this individual identifier. + + Returns + ------- + the individual identifier : :class:`str` + + .. versionadded:: 0.0.1 + """ + raise NotImplementedError + + @property + @abstractmethod + def population(self) -> P: + """ + Get the underlying population. + + Returns + ------- + the underlying population : :class:`P` + + .. versionadded:: 0.0.1 + """ + raise NotImplementedError + + @property + def context(self) -> C: + """ + Get the underlying context. + + Returns + ------- + the underlying context : :class:`C` + + .. versionadded:: 0.0.1 + """ + return self.population.context + + @property + def model(self) -> M: + """ + Get the underlying model. + + Returns + ------- + the underlying model : :class:`M` + + .. versionadded:: 0.0.1 + """ + return self.context.model + + def value(self, attribute: A): + """ + Get the attribute value for this individual. + + Parameters + ---------- + attribute : :class:`A` + the attribute + + Returns + ------- + the value : :class:`object` + + Raises + ------ + ValueError + if the attribute does not belong to the underlying model. + + .. versionadded:: 0.0.1 + """ + if attribute.context == self.context: + return self[attribute.name] + else: + raise ValueError + + @abstractmethod + def __getitem__(self, name: str): + """ + Get the value of this individual for the given attribute. The :param name: is the attribute + name. + + Parameters + ---------- + name : :class:`str` + the attribute name + + Returns + ------- + the associated value : :class:`object` + + Raises + ------ + KeyError + if the attribute does not belong to the underlying model. + + .. versionadded:: 0.0.1 + """ + raise NotImplementedError + + def __iter__(self) -> Iterator[str]: + """ + Get an iterator for this individual. + + Returns + ------- + an iterator : :class:`` + + .. versionadded:: 0.0.1 + """ + return iter(self.model) + + def __len__(self): + """ + Get the length of this individual. + + Returns + ------- + the length of this individual : :class:`int` + + .. versionadded:: 0.0.1 + """ + return len(self.model) + + def __eq__(self, other: X): + """ + Check if two individuals are equal. + + Parameters + ---------- + other : :class:`X` + the individual to test the equality with + + Returns + ------- + self == other : :class:`bool` + + .. versionadded:: 0.0.1 + """ + return self.identifier == other.identifier and self.context == other.context + + def __hash__(self): + """ + Use specific hashing. + + Returns + ------- + the hash number : :class:`int` + + .. versionadded:: 0.0.1 + """ + return hash((self.identifier, self.context)) + + def __str__(self): + """ + Convert this individual to a readable string. + + Returns + ------- + the user friendly readable string of this individual : :class:`str` + + .. versionadded:: 0.0.1 + """ + return str({name: value for name, value in self.items()}) + + +# pylint: disable=too-few-public-methods,function-redefined +class Attribute(Generic[C, P, M, X, A], Mapping[str, object]): + """ + A :class:`Attribute` is described by a :attr:`name` and a :attr:`type`. + + It's possible to access to the attribute :attr:`identifier` and :attr:`type` attributes. + + Example + ------- + + >>> from galactic.context.memory import MemoryContext + >>> context = MemoryContext({'mybool': bool, 'myint': int}, ['0', '1']) + >>> attribute = context.model['mybool'] + >>> attribute.name + 'mybool' + >>> attribute.type + + + It's possible to access to the individual :attr:`context` attribute. + + Example + ------- + + >>> print(attribute.context) + {'population': ['0', '1'], 'model': {'mybool': , 'myint': }} + + It's possible to access to the individual :attr:`model` attribute. + + Example + ------- + + >>> print(attribute.model) + {'mybool': , 'myint': } + + It's possible to access to the individual :attr:`population` attribute. + + Example + ------- + + >>> print(attribute.population) + ['0', '1'] + + It's possible to get a readable representation of an attribute using the python builtin + :func:`str` function. + + Example + ------- + + >>> print(attribute) + {'name': 'mybool': 'type': } + + It's possible to access to the attribute values using the :meth:`value` method. + + Example + ------- + + >>> individual = attribute.population['0'] + >>> attribute.value(individual) + False + + It's possible to access to the attribute values using the python array access + construct. + + Example + ------- + + >>> attribute['0'] + False + + It's possible to get the length of an attribute using the python builtin :func:`len` + function. + + Example + ------- + + >>> len(attribute) + 2 + + It's possible to iterate over an attribute using the python :keyword:`for` keyword. + + Example + ------- + + >>> {identifier: value for identifier, value in attribute.items()} + {'0': False, '1': False} + + .. versionadded:: 0.0.1 + """ + + @property + @abstractmethod + def name(self) -> str: + """ + Get the attribute name. + + Returns + ------- + the attribute name : :class:`str` + + .. versionadded:: 0.0.1 + """ + raise NotImplementedError + + @property + @abstractmethod + def type(self) -> type: + """ + Get the attribute type. + + Returns + ------- + the attribute type : :class:`type ` + + .. versionadded:: 0.0.1 + """ + raise NotImplementedError + + @property + @abstractmethod + def model(self) -> M: + """ + Get the underlying model. + + Returns + ------- + the underlying model : :class:`M` + + .. versionadded:: 0.0.1 + """ + raise NotImplementedError + + @property + def context(self) -> C: + """ + Get the underlying context. + + Returns + ------- + the underlying context : :class:`C` + + .. versionadded:: 0.0.1 + """ + return self.model.context + + @property + def population(self) -> P: + """ + Get the underlying population. + + Returns + ------- + the underlying population : :class:`P` + + .. versionadded:: 0.0.1 + """ + return self.context.population + + def value(self, individual: X): + """ + Get the individual value for this attribute. + + Parameters + ---------- + individual : :class:`X` + the individual + + Returns + ------- + the value : :class:`object` + + Raises + ------ + ValueError + if the individual does not belong to the underlying population. + + .. versionadded:: 0.0.1 + """ + return individual.value(self) + + def __getitem__(self, identifier: str): + """ + Get the value of this attribute for the given individual. + + Parameters + ---------- + identifier : :class:`str` + the individual identifier + + Returns + ------- + the value : :class:`object` + + Raises + ------ + KeyError + if the individual does not belong to the underlying population. + + .. versionadded:: 0.0.1 + """ + return self.population[identifier][self.name] + + def __iter__(self) -> Iterator[str]: + """ + Get the iterator of this attribute. + + Returns + ------- + an iterator : :class:`Iterator[Individual] ` + + .. versionadded:: 0.0.1 + """ + return iter(self.population) + + def __len__(self): + """ + Get the length of this attribute. + + Returns + ------- + the length of this attribute : :class:`int` + + .. versionadded:: 0.0.1 + """ + return len(self.population) + + def __eq__(self, other: A): + """ + Check if two attributes are equal. + + Parameters + ---------- + other : :class:`A` + the attribute to test the equality with + + Returns + ------- + self == other : :class:`bool` + + .. versionadded:: 0.0.1 + """ + return self.name == other.name and self.context == other.context + + def __hash__(self): + """ + Use specific hashing. + + Returns + ------- + the hash number : :class:`int` + + .. versionadded:: 0.0.1 + """ + return hash((self.name, self.context)) + + def __str__(self): + """ + Convert this attribute to a printable string. + + Returns + ------- + the user friendly readable string of this attribute : :class:`str` + + .. versionadded:: 0.0.1 + """ + return str({'name': self.name, 'type': self.type}) diff --git a/galactic/context/memory/__init__.py b/galactic/context/memory/__init__.py new file mode 100644 index 0000000..1af0a05 --- /dev/null +++ b/galactic/context/memory/__init__.py @@ -0,0 +1,497 @@ +# This Python file uses the following encoding: utf-8 + +""" +The :mod:`galactic.context.memory` module give the ability to define +:class:`Context ` that resides in memory. +""" + +from typing import Mapping, Iterable, Union + +from galactic.context import Context, Model, Population, Attribute, Individual +from galactic.context.mixins import ContextHolder, PopulationHolder, ModelHolder, \ + ConcreteAttribute, ConcreteIndividual, AttributesHolder, IndividualsHolder, ValuesHolder + + +# pylint: disable=too-few-public-methods,abstract-method,super-init-not-called +class MemoryContext( + PopulationHolder['MemoryPopulation'], + ModelHolder['MemoryModel'], + Context['MemoryPopulation', 'MemoryModel', 'MemoryIndividual', 'MemoryAttribute'] +): + """ + The :class:`MemoryContext` class is designed to define contexts in memory. It inherits of all + the behavior from the :class:`Context ` class and allows direct + creation and modification of a context. + + It's possible to create a context without nothing. + + Example + ------- + + >>> from galactic.context.memory import MemoryContext + >>> context = MemoryContext() + >>> print(context) + {'population': [], 'model': {}} + + It's possible to create a context specifying the model. + + Example + ------- + + >>> from galactic.context.memory import MemoryContext + >>> context = MemoryContext({'mybool': bool, 'myint': int}) + >>> print(context) + {'population': [], 'model': {'mybool': , 'myint': }} + + It's possible to create a context specifying the model and the list of individual identifiers. + + Example + ------- + + >>> from galactic.context.memory import MemoryContext + >>> context = MemoryContext({'mybool': bool, 'myint': int}, ['0', '1']) + >>> print(context) + {'population': ['0', '1'], 'model': {'mybool': , 'myint': }} + + It's possible to create a context specifying the model and the individual values. + + Example + ------- + + >>> from galactic.context.memory import MemoryContext + >>> context = MemoryContext( + ... {'mybool': bool, 'myint': int}, + ... {'0': {'mybool': True}, '1':{'myint': 1}} + ... ) + >>> {ident: str(context.population[ident]) for ident in context.population} + {'0': "{'mybool': True, 'myint': 0}", '1': "{'mybool': False, 'myint': 1}"} + + .. versionadded:: 0.0.1 + """ + + def __init__( + self, + definition: Mapping[str, type] = None, + individuals: Union[Iterable[str], Mapping[str, Mapping[str, object]]] = None + ): + """ + Initialise a context in memory. + + Parameters + ---------- + definition : :class:`Mapping[str, type] ` + definition of the context by a mapping from name of attributes to their type + individuals : :class:`Union[Iterable[str], Mapping[str, Mapping[str, object]]]` + initial iterable of individual identifiers or a mapping from individual + identifiers to individual values + + Raises + ------ + KeyError + if an attribute is not in the definition + ValueError + if a value does not correspond to an attribute type + + .. versionadded:: 0.0.1 + """ + super().__init__( + model=MemoryModel(self, {} if definition is None else definition), + population=MemoryPopulation(self, []) + ) + + if isinstance(individuals, Mapping): + for identifier, values in individuals.items(): + self.population[identifier] = values + else: + if individuals is not None: + for identifier in individuals: + self.population[identifier] = {} + + +# pylint: disable=too-few-public-methods,abstract-method,super-init-not-called +class MemoryModel( + ContextHolder[MemoryContext], + AttributesHolder['MemoryAttribute'], + Model[MemoryContext, 'MemoryPopulation', 'MemoryAttribute'] +): + """ + The :class:`MemoryModel` class is designed to define models that resides in memory. It inherits + of all the behavior from the :class:`Model ` class. + + It's possible to change or to set attribute values. + + Example + ------- + + >>> from galactic.context.memory import MemoryContext + >>> context = MemoryContext({'mybool': bool, 'myint': int}, ['0', '1']) + >>> model = context.model + >>> model['mybool'] = int + >>> model['myint2'] = int + >>> print(context.population['0']) + {'mybool': 0, 'myint': 0, 'myint2': 0} + + It's possible to delete an attribute using its name. + + Example + ------- + + >>> from galactic.context.memory import MemoryContext + >>> context = MemoryContext({'mybool': bool, 'myint': int}, ['0', '1']) + >>> model = context.model + >>> del model['mybool'] + >>> {ident: str(context.population[ident]) for ident in context.population} + {'0': "{'myint': 0}", '1': "{'myint': 0}"} + + + .. versionadded:: 0.0.1 + """ + + def __init__( + self, + context: MemoryContext, + definition: Mapping[str, 'type'] + ): + """ + Initialise a model in memory. + + Parameters + ---------- + context : :class:`MemoryContext` + the underlying context + definition : :class:`Mapping[str, type] ` + the attributes definition + + .. versionadded:: 0.0.1 + """ + super().__init__( + context=context, + attributes=[MemoryAttribute(self, name, cls) for name, cls in definition.items()] + ) + + def __delitem__(self, name: str): + """ + Delete an attribute from a :class:`MemoryModel`. + + Parameters + ---------- + name : :class:`str` + attribute name + + Raises + ------ + KeyError + if the attribute is not in the model + + .. versionadded:: 0.0.1 + """ + super().__delitem__(name) + for identifier in self.population: + del self.population[identifier][name] + + def __setitem__(self, name: str, cls: type): + """ + Set an attribute for a :class:`MemoryModel`. + + Parameters + ---------- + name : :class:`str` + the attribute name + cls : :class:`python:type` + the attribute type + + .. versionadded:: 0.0.1 + """ + if name in self: + if self[name].type != cls: + super().__setitem__(name, MemoryAttribute(self, name, cls)) + for identifier in self.population: + try: + self.population[identifier][name] = cls(self.population[identifier][name]) + except (ValueError, TypeError): + self.population[identifier][name] = cls() + else: + super().__setitem__(name, MemoryAttribute(self, name, cls)) + for identifier in self.population: + self.population[identifier][name] = cls() + + +# pylint: disable=too-few-public-methods,abstract-method,super-init-not-called +class MemoryPopulation( + ContextHolder[MemoryContext], + IndividualsHolder['MemoryIndividual'], + Population[MemoryContext, MemoryModel, 'MemoryIndividual'] +): + """ + The :class:`MemoryPopulation` class is designed to define populations that resides in memory. It + inherits of all the behavior from the :class:`Population ` class. + + It's possible to change or to set individual values. + + Example + ------- + + >>> from galactic.context.memory import MemoryContext + >>> context = MemoryContext({'mybool': bool, 'myint': int}, ['0', '1']) + >>> population = context.population + >>> population['0'] = {'mybool': True} + >>> population['2'] = {'myint': 1} + >>> print(population['0']) + {'mybool': True, 'myint': 0} + >>> print(population['2']) + {'mybool': False, 'myint': 1} + + It's possible to delete an individual using its identifier. + + Example + ------- + + >>> from galactic.context.memory import MemoryContext + >>> context = MemoryContext({'mybool': bool, 'myint': int}, ['0', '1']) + >>> population = context.population + >>> del population['0'] + >>> {ident: str(context.population[ident]) for ident in context.population} + {'1': "{'mybool': False, 'myint': 0}"} + + .. versionadded:: 0.0.1 + """ + + def __init__( + self, + context: MemoryContext, + identifiers: Iterable[str] + ): + """ + Initialise a population in memory. + + Parameters + ---------- + context : :class:`MemoryContext` + the underlying context + identifiers : :class:`Iterable[str] ` + an iterable of identifiers + + .. versionadded:: 0.0.1 + """ + super().__init__( + context=context, + individuals=[MemoryIndividual(self, identifier) for identifier in identifiers] + ) + + def __setitem__(self, identifier: str, values: Mapping[str, object]): + """ + Set an individual for a :class:`MemoryPopulation`. + + Parameters + ---------- + identifier : :class:`str` + the individual identifier + values : :class:`Mapping[str, object] <>` + the initial values for some attributes + + Raises + ------ + KeyError + if an attribute name does not belong to the underlying model. + ValueError + if an attribute value does not correspond to its type + + .. versionadded:: 0.0.1 + """ + try: + individual = self[identifier] + except KeyError: + individual = MemoryIndividual(self, identifier) + super().__setitem__(identifier, individual) + for name, value in values.items(): + if name not in self.model: + raise KeyError + individual[name] = value + + +# pylint: disable=too-few-public-methods,abstract-method,super-init-not-called +class MemoryIndividual( + PopulationHolder[MemoryPopulation], + ValuesHolder, + ConcreteIndividual, + Individual[ + MemoryContext, + MemoryPopulation, + MemoryModel, + 'MemoryIndividual', + 'MemoryAttribute' + ] +): + """ + The :class:`MemoryIndividual` is designed to define individuals that resides in memory. It + inherits of all the behavior from the :class:`Individual ` class. + + It's possible to modify a value for an individual using an attribute name. + + Example + ------- + + >>> from galactic.context.memory import MemoryContext + >>> context = MemoryContext({'mybool': bool, 'myint': int}, ['0', '1']) + >>> individual = context.population['0'] + >>> individual['mybool'] = True + >>> individual['myint'] = 1 + >>> {ident: str(context.population[ident]) for ident in context.population} + {'0': "{'mybool': True, 'myint': 1}", '1': "{'mybool': False, 'myint': 0}"} + + .. versionadded:: 0.0.1 + """ + + def __init__( + self, + population: MemoryPopulation, + identifier: str + ): + """ + Initialise an individual. + + Parameters + ---------- + population : :class:`MemoryPopulation` + the population + identifier : :class:`str` + the individual identifier + + .. versionadded:: 0.0.1 + """ + super().__init__( + population=population, + identifier=identifier, + values={name: attribute.type() for name, attribute in population.model.items()} + ) + + def __setitem__(self, name: str, value): + """ + Set an individual's attribute value using either the attribute or its name. + + Parameters + ---------- + name: :class:`str` + the attribute name + value : :class:`object` + the value + + Raises + ------ + KeyError + if the attribute does not belong to the underlying context + ValueError + if the value passed in argument has its type different of the attribute type + + .. versionadded:: 0.0.1 + """ + attribute = self.model[name] + if not isinstance(value, attribute.type): + value = attribute.type(value) + super().__setitem__(name, value) + + def __delitem__(self, name: str): + """ + Delete an individual value. + + Parameters + ---------- + name: :class:`str` + the attribute name + + Raises + ------ + ValueError + if the attribute is in the model (which should be always the case) + + .. versionadded:: 0.0.1 + """ + if name in self.model: + raise ValueError + else: + super().__delitem__(name) + + +# pylint: disable=too-few-public-methods,abstract-method,super-init-not-called +class MemoryAttribute( + ModelHolder[MemoryModel], + ConcreteAttribute, + Attribute[ + MemoryContext, + MemoryPopulation, + MemoryModel, + MemoryIndividual, + 'MemoryAttribute' + ] +): + """ + The :class:`MemoryAttribute` is designed to define attributes that resides in memory. It + inherits of all the behavior from the :class:`Attribute ` class. + + It's possible to modify a value for an attribute using an individual identifier. + + Example + ------- + + >>> from galactic.context.memory import MemoryContext + >>> context = MemoryContext({'mybool': bool, 'myint': int}, ['0', '1']) + >>> attribute = context.model['myint'] + >>> attribute['0'] = 3 + >>> attribute['1'] = 4 + >>> {ident: str(context.population[ident]) for ident in context.population} + {'0': "{'mybool': False, 'myint': 3}", '1': "{'mybool': False, 'myint': 4}"} + + .. versionadded:: 0.0.1 + """ + + def __init__( + self, + model: MemoryModel, + name: str, + cls: 'type' + ): + """ + Initialise an attribute. + + Parameters + ---------- + model : :class:`MemoryModel` + the underlying model + name : :class:`str` + the attribute name + cls : `type ` + the attribute type + + .. versionadded:: 0.0.1 + """ + super().__init__( + model=model, + name=name, + type=cls + ) + + def __setitem__(self, identifier: str, value): + """ + Set an individual's attribute value using either the individual or its identifier. + + This method raises a :class:`KeyError` exception if the attribute does not belong to + the underlying context and a :class:`ValueError` exception if the value passed in argument + has its type different of the attribute type. + + Parameters + ---------- + identifier : :class:`str` + the individual identifier + value : :class:`object` + the value + + Raises + ------ + KeyError + if the individual does not exists + ValueError + if the value is not of the attribute type + + .. versionadded:: 0.0.1 + """ + self.population[identifier][self.name] = value diff --git a/galactic/context/mixins.py b/galactic/context/mixins.py new file mode 100644 index 0000000..85b6190 --- /dev/null +++ b/galactic/context/mixins.py @@ -0,0 +1,570 @@ +# This Python file uses the following encoding: utf-8 +""" +The :mod:`galactic.context.mixins` package defines mixins classes for defining new types of +contexts: + +* :class:`ConcreteIndividual` for defining individuals that own their identifier as a field +* :class:`ConcreteAttribute` for defining attributes that own their name and their type as fields +* :class:`ContextHolder` for defining elements that own their context as a field +* :class:`PopulationHolder` for defining elements that own their population as a field +* :class:`ModelHolder` for defining elements that own their model as a field +* :class:`AttributesHolder` for defining models that own their attributes as a field +* :class:`IndividualsHolder` for defining population that own their individuals as a field +* :class:`ValuesHolder` for defining individuals that own their values as a field + +They are widely used for defining + +* :class:`MemoryContext ` +* :class:`MemoryModel ` +* :class:`MemoryPopulation ` +* :class:`MemoryAttribute ` +* :class:`MemoryIndividual ` + +in the :mod:`galactic.context.memory` package. + +.. versionadded:: 0.0.1 +""" + +from typing import Generic, MutableMapping, Iterator + +from galactic.context import C, P, M, A, X + + +# pylint: disable=too-few-public-methods +class _Mixin(object): + def __init__(self, **_): + super().__init__() + + +# pylint: disable=too-few-public-methods,function-redefined +class ConcreteIndividual(_Mixin): + """ + The :class:`ConcreteIndividual` class is a mixin used in subclassing the + :class:`Individual ` class for storing their identifier as a field. + + .. versionadded:: 0.0.1 + """ + + def __init__(self, **kwargs): + """ + Initialise an individual. + + Keyword Arguments + ----------------- + identifier : :class:`str` + the individual identifier + + .. versionadded:: 0.0.1 + """ + super().__init__(**kwargs) + self._identifier = kwargs['identifier'] + + @property + def identifier(self) -> str: + """ + Get the individual identifier. + + Returns + ------- + the individual identifier : :class:`str` + + .. versionadded:: 0.0.1 + """ + return self._identifier + + +# pylint: disable=too-few-public-methods +class ConcreteAttribute(_Mixin): + """ + The :class:`ConcreteAttribute` class is a mixin used in subclassing the + :class:`Attribute ` class for storing their name and their type + as a class. + + .. versionadded:: 0.0.1 + """ + + def __init__(self, **kwargs): + """ + Initialise an attribute + + Keyword Arguments + ----------------- + name : :class:`str` + the attribute name + + cls : :class:`type ` + the attribute type + + .. versionadded:: 0.0.1 + """ + super().__init__(**kwargs) + self._name = kwargs['name'] + self._type = kwargs['type'] + + @property + def name(self) -> str: + """ + Get the attribute name. + + Returns + ------- + the attribute name : :class:`str` + + .. versionadded:: 0.0.1 + """ + return self._name + + @property + def type(self) -> type: + """ + Get the attribute type. + + Returns + ------- + the attribute type : :class:`type ` + + .. versionadded:: 0.0.1 + """ + return self._type + + +# pylint: disable=too-few-public-methods +class ContextHolder(_Mixin, Generic[C]): + """ + The :class:`ContextHolder[C] ` is a mixin used for storing an element context as + a field. + + It's a generic class that depends of a :class:`Context ` subclass + :class:`C`. + + .. versionadded:: 0.0.1 + """ + + def __init__(self, **kwargs): + """ + Initialise an element by setting its context. + + Keyword Arguments + ----------------- + context : :class:`C` + the context + + .. versionadded:: 0.0.1 + """ + super().__init__(**kwargs) + self._context = kwargs['context'] + + # noinspection PyTypeChecker + @property + def context(self) -> C: + """ + Get the context. + + Returns + ------- + the context : :class:`C` + + .. versionadded:: 0.0.1 + """ + return self._context + + +# pylint: disable=too-few-public-methods +class PopulationHolder(_Mixin, Generic[P]): + """ + The :class:`PopulationHolder[P] ` is a mixin used for storing an element + population as a field. + + It's a generic class that depends of a :class:`Population ` + subclass :class:`P`. + + .. versionadded:: 0.0.1 + """ + + def __init__(self, **kwargs): + """ + Initialise an element by setting its population. + + Keyword Arguments + ----------------- + population: :class:`P` + the population + + .. versionadded:: 0.0.1 + """ + super().__init__(**kwargs) + self._population = kwargs['population'] + + # noinspection PyTypeChecker + @property + def population(self) -> P: + """ + Get the population. + + Returns + ------- + the population : :class:`P` + + .. versionadded:: 0.0.1 + """ + return self._population + + +# pylint: disable=too-few-public-methods +class ModelHolder(_Mixin, Generic[M]): + """ + The :class:`ModelHolder[M] ` class is a mixin used for storing an element model as + a field. + + It's a generic class that depends of a :class:`Model ` subclass + :class:`M`. + + .. versionadded:: 0.0.1 + """ + + def __init__(self, **kwargs): + """ + Initialise an element by setting its model. + + Keyword Arguments + ----------------- + model : :class:`M` + the model + + .. versionadded:: 0.0.1 + """ + super().__init__(**kwargs) + self._model = kwargs['model'] + + # noinspection PyTypeChecker + @property + def model(self) -> M: + """ + Get the model. + + Returns + ------- + the model : :class:`M` + + .. versionadded:: 0.0.1 + """ + return self._model + + +# pylint: disable=too-few-public-methods +class AttributesHolder(_Mixin, Generic[A], MutableMapping[str, A]): + """ + The :class:`AttributesHolder[A] ` class is a mixin used for storing the model + attributes in memory. + + It's a generic class that depends of an :class:`Attribute ` + subclass :class:`A`. + + .. versionadded:: 0.0.1 + """ + + def __init__(self, **kwargs): + """ + Initialise an attribute holder. + + Keyword Arguments + ----------------- + attributes : :class:`Iterable[A] ` + the attributes + + .. versionadded:: 0.0.1 + """ + super().__init__(**kwargs) + self._attributes = {attribute.name: attribute for attribute in kwargs['attributes']} + + def __getitem__(self, name: str) -> A: + """ + Get an attribute using its name + + Parameters + ---------- + name : :class:`str` + the attribute name + + Returns + ------- + the attribute : A + + Raises + ------ + KeyError + if this attributes holder does not contain an attribute with this name. + + .. versionadded:: 0.0.1 + """ + return self._attributes[name] + + def __setitem__(self, name: str, attribute: A) -> None: + """ + Set an attribute using its name + + Parameters + ---------- + name : :class:`str` + the attribute name + attribute: :class:`A` + + .. versionadded:: 0.0.1 + """ + self._attributes[name] = attribute + + def __delitem__(self, name: str) -> None: + """ + Delete an attribute using its name + + Parameters + ---------- + name : :class:`str` + the attribute name + + Raises + ------ + KeyError + if this attributes holder does not contain an attribute with this name. + + .. versionadded:: 0.0.1 + """ + del self._attributes[name] + + def __iter__(self) -> Iterator[str]: + """ + Get an iterator over the attribute names. + + Returns + ------- + an iterator : :class:`Iterator[str] ` + + .. versionadded:: 0.0.1 + """ + return iter(self._attributes) + + def __len__(self) -> int: + """ + Get the number of attributes. + + Returns + ------- + the number of attributes : :class:`int` + + .. versionadded:: 0.0.1 + """ + return len(self._attributes) + + def __bool__(self): + """ + Get the boolean value of an attribute holder. + + Returns + ------- + the boolean value : :class:`bool` + + .. versionadded:: 0.0.1 + """ + return bool(self._attributes) + + +# pylint: disable=too-few-public-methods +class IndividualsHolder(_Mixin, Generic[X], MutableMapping[str, X]): + """ + The :class:`IndividualsHolder[X] ` class is a mixin used for storing the + population individuals in memory. + + It's a generic class that depends of an :class:`Individual ` + subclass :class:`X`. + + .. versionadded:: 0.0.1 + """ + + def __init__(self, **kwargs): + """ + Initialise an individuals holder. + + Keyword Arguments + ----------------- + individuals : :class:`Iterable[A] ` + the individuals + + .. versionadded:: 0.0.1 + """ + super().__init__(**kwargs) + self._individuals = {individual.identifier: individual for individual in + kwargs['individuals']} + + def __getitem__(self, identifier: str) -> X: + """ + Get an individual using its identifier. + + Parameters + ---------- + identifier : :class:`str` + the individual identifier + + Returns + ------- + the individual : :class: `X` + + Raises + ------ + KeyError + if the identifier does not belong to the individuals holder. + + .. versionadded:: 0.0.1 + """ + return self._individuals[identifier] + + def __delitem__(self, identifier: str) -> None: + """ + Delete an individual using its identifier. + + Parameters + ---------- + identifier : :class:`str` + the individual identifier + + Raises + ------ + KeyError + if the identifier does not belong to the individuals holder. + + .. versionadded:: 0.0.1 + """ + del self._individuals[identifier] + + def __setitem__(self, identifier: str, individual: X) -> None: + """ + Set an individual using its identifier. + + Parameters + ---------- + identifier : :class:`str` + the individual identifier + + individual: :class:`X` + an individual + + .. versionadded:: 0.0.1 + """ + self._individuals[identifier] = individual + + def __iter__(self) -> Iterator[str]: + """ + Get an iterator over the individual identifiers. + + Returns + ------- + an iterator : :class:`Iterator[str] ` + + .. versionadded:: 0.0.1 + """ + return iter(self._individuals) + + def __len__(self) -> int: + """ + Get the number of individuals. + + Returns + ------- + the number of indidivuals : :class:`int` + + .. versionadded:: 0.0.1 + """ + return len(self._individuals) + + def __bool__(self) -> bool: + """ + Get the boolean representation of the individuals holder. + + Returns + ------- + the boolean representation : :class:`bool` + + .. versionadded:: 0.0.1 + """ + return bool(self._individuals) + + +# pylint: disable=too-few-public-methods +class ValuesHolder(_Mixin): + """ + The :class:`ValuesHolder[A] ` class is a mixin for storing the individual values + in memory. + + It's a generic class that depends of an :class:`Attribute ` subclass + :class:`A`. + + .. versionadded:: 0.0.1 + """ + + def __init__(self, **kwargs): + """ + Initialise a values holder. + + Keyword Arguments + ----------------- + values : :class:`Mapping[str, object] ` + the initial (name, value) pairs + + .. versionadded:: 0.0.1 + """ + super().__init__(**kwargs) + self._values = {name: value for name, value in kwargs['values'].items()} + + def __getitem__(self, name: str) -> object: + """ + Get a value using the attribute name. + + Parameters + ---------- + name : :class:`str` + the attribute name + + Returns + ------- + the value : :class:`object` + + Raises + ------ + KeyError + if the name does not belong to the values holder. + + .. versionadded:: 0.0.1 + """ + return self._values[name] + + def __setitem__(self, name: str, value) -> None: + """ + Set the value of an attribute. + + Parameters + ---------- + name : :class:`str` + the attribute name + value : :class:`object` + the new value + + .. versionadded:: 0.0.1 + """ + self._values[name] = value + + def __delitem__(self, name: str): + """ + Delete a value using the attribute name. + + Parameters + ---------- + name : :class:`str` + the attribute name + + Raises + ------ + KeyError + if the name does not belong to the values holder. + + .. versionadded:: 0.0.1 + """ + del self._values[name] diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..ee9c50a --- /dev/null +++ b/setup.cfg @@ -0,0 +1,11 @@ +[bdist_wheel] +# This flag says that the code is written to work on both Python 2 and Python +# 3. If at all possible, it is good practice to do this. If you cannot, you +# will need to generate wheels for each Python version that you support. +universal=1 + +[metadata] +description-file = README.md + +[aliases] +test=pytest diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..be6e769 --- /dev/null +++ b/setup.py @@ -0,0 +1,143 @@ +# This Python file uses the following encoding: utf-8 + +"""A setuptools based setup module. + +See: +https://packaging.python.org/en/latest/distributing.html +https://github.com/thegalactic/py-galactic +""" + +# To use a consistent encoding +from codecs import open +from os import path + +# Always prefer setuptools over distutils +from setuptools import setup + +here = path.abspath(path.dirname(__file__)) + +# Get the long description from the README file +try: + import pypandoc + + long_description = pypandoc.convert('README.md', 'rst') +except (IOError, ImportError): + with open(path.join(here, 'README.md'), encoding='utf-8') as f: + long_description = f.read() + +try: + from sphinx.setup_command import BuildDoc + cmdclass = {'build_sphinx': BuildDoc} +except ImportError: + cmdclass = {} + +version = '0.0.1' +release = '0.0.1' +name = 'py-galactic' +setup( + name=name, + + # Versions should comply with PEP440. For a discussion on single-sourcing + # the version across setup.py and the project code, see + # https://packaging.python.org/en/latest/single_source_version.html + + version=release, + cmdclass=cmdclass, + # these are optional and override conf.py settings + command_options={ + 'build_sphinx': { + 'project': ('setup.py', name), + 'version': ('setup.py', version), + 'release': ('setup.py', release) + } + }, + + # The project's description + description='A package for Formal Context Analysis', + long_description=long_description, + + # The project's main homepage. + url='https://github.com/thegalactic/py-galactic', + + # The project's download page + download_url='https://github.com/thegalactic/py-galactic/archive/master.zip', + + # Author details + author='The Galactic Organization', + author_email='contact@thegalactic.org', + + # Maintainer details + maintainer='The Galactic Organization', + maintainer_email='contact@thegalactic.org', + + # Choose your license + license='BSD-3-Clause', + + # See https://pypi.python.org/pypi?%3Aaction=list_classifiers + classifiers=[ + # How mature is this project? Common values are + # 1 - Planning + # 2 - Pre-Alpha + # 3 - Alpha + # 4 - Beta + # 5 - Production/Stable + # 6 - Mature + # 7 - Inactive + 'Development Status :: 2 - Pre-Alpha', + + # Specify the OS + 'Operating System :: OS Independent', + + # Indicate who your project is intended for + 'Environment :: Console', + 'Intended Audience :: End Users/Desktop', + 'Intended Audience :: Developers', + + # Specify the Python versions you support here. In particular, ensure + # that you indicate whether you support Python 2, Python 3 or both. + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + + # Natural language used + 'Natural Language :: English', + ], + + # What does your project relate to? + keywords='formal context analysis', + + # List run-time dependencies here. These will be installed by pip when + # your project is installed. For an analysis of "install_requires" vs pip's + # requirements files see: + # https://packaging.python.org/en/latest/requirements.html + install_requires=[ + 'bitstring>=3.1' + ], + + # List additional groups of dependencies here (e.g. development + # dependencies). You can install these using the following syntax, + # for example: + # $ pip install -e .[dev,test] + extras_require={ + 'dev': ['check-manifest'], + 'test': ['coverage3'], + }, + + # If there are data files included in your packages that need to be + # installed, specify them here. If using Python 2.6 or less, then these + # have to be included in MANIFEST.in as well. + package_data={}, + + setup_requires=[ + 'pytest-runner', + 'pypandoc>=1.4', + 'sphinx>=1.6', + 'sphinx_rtd_theme>=0.2.4' + ], + tests_require=['pytest', 'coverage'], + + packages=[ + 'galactic', + 'galactic.context', + 'galactic.context.memory' + ], +) diff --git a/tests/context/memory/test_memory.py b/tests/context/memory/test_memory.py new file mode 100644 index 0000000..721e2ee --- /dev/null +++ b/tests/context/memory/test_memory.py @@ -0,0 +1,506 @@ +# This Python file uses the following encoding: utf-8 + +from unittest import TestCase + +from galactic.context.memory import * + + +class MemoryContextTest(TestCase): + def test___init__(self): + context = MemoryContext({'mybool': bool, 'myint': int}) + self.assertEqual( + str(context), + "{'population': [], 'model': {'mybool': , 'myint': }}", + "The context has not been correctly initialized" + ) + context = MemoryContext({'mybool': bool, 'myint': int}, ['0', '1']) + self.assertEqual( + str(context.population), + "['0', '1']", + "The context has not been correctly initialized" + ) + self.assertEqual( + str(context.model), + "{'mybool': , 'myint': }", + "The context has not been correctly initialized" + ) + context = MemoryContext( + {'mybool': bool, 'myint': int}, + {'0': {'mybool': True}, '1': {'myint': 1}} + ) + self.assertEqual( + str(context.population['0']), + "{'mybool': True, 'myint': 0}", + "The context has not been correctly initialized" + ) + self.assertEqual( + str(context.population['1']), + "{'mybool': False, 'myint': 1}", + "The context has not been correctly initialized" + ) + + def test_population(self): + context = MemoryContext({'mybool': bool, 'myint': int}) + self.assertTrue( + isinstance(context.population, Population), + "The property population is instance of Population" + ) + + def test_model(self): + context = MemoryContext({'mybool': bool, 'myint': int}) + self.assertTrue( + isinstance(context.model, Model), + "The property model is instance of Model" + ) + + def test___contains__(self): + context = MemoryContext({'mybool': bool, 'myint': int}, ['0', '1']) + self.assertTrue( + context.population['0'] in context, + "The first individual of the context is in the context" + ) + self.assertTrue( + context.model['mybool'] in context, + "The first attribute of the context is in the context" + ) + other = MemoryContext({'mybool': bool, 'myint': int}, ['0', '1']) + self.assertFalse( + other.population['0'] in context, + "An unknown element is not in the context" + ) + + def test___bool__(self): + context = MemoryContext({'mybool': bool, 'myint': int}, ['0', '1']) + self.assertTrue( + context, + "The context is not empty" + ) + context = MemoryContext({'mybool': bool, 'myint': int}) + self.assertFalse( + context, + "The context is empty" + ) + context = MemoryContext({}, ['0', '1']) + self.assertFalse( + context, + "The context is empty" + ) + + def test___str__(self): + context = MemoryContext({'mybool': bool}, ['0']) + self.assertEqual( + str(context), + "{'population': ['0'], 'model': {'mybool': }}", + "The string representation of this context is not correct" + ) + + +class MemoryPopulationTest(TestCase): + def test_context(self): + context = MemoryContext({'mybool': bool, 'myint': int}, ['0', '1']) + self.assertEqual( + context.population.context, + context, + "The context of the population must be the original context" + ) + + def test_model(self): + context = MemoryContext({'mybool': bool, 'myint': int}, ['0', '1']) + self.assertEqual( + context.population.model, + context.model, + "The model of the population must be the original model" + ) + + def test___getitem__(self): + context = MemoryContext({'mybool': bool, 'myint': int}, ['0', '1']) + self.assertEqual( + str(context.population['0']), + "{'mybool': False, 'myint': 0}", + "The textual representation of the first individual is not correct" + ) + + def test___setitem__(self): + context = MemoryContext({'mybool': bool, 'myint': int}) + context.population['0'] = {} + self.assertEqual( + str(context.population['0']), + "{'mybool': False, 'myint': 0}", + "The population is not correct" + ) + context.population['0'] = {'mybool': True} + self.assertEqual( + str(context.population['0']), + "{'mybool': True, 'myint': 0}", + "The population is not correct" + ) + with self.assertRaises(KeyError): + context.population['0'] = {'abc': 1} + + def test___delitem__(self): + context = MemoryContext({'mybool': bool, 'myint': int}, ['0']) + del context.population['0'] + self.assertFalse( + bool(context.population), + "The population must be empty" + ) + + def test___len__(self): + context = MemoryContext({'mybool': bool, 'myint': int}, ['0', '1']) + self.assertEqual( + len(context.population), + 2, + "The length of the population is not correct" + ) + + def test___bool__(self): + context = MemoryContext({'mybool': bool, 'myint': int}) + self.assertFalse( + bool(context.population), + "The population must be empty" + ) + context.population['0'] = {} + self.assertTrue( + bool(context.population), + "The population must be not empty" + ) + + def test___iter__(self): + context = MemoryContext({'mybool': bool, 'myint': int}, ['0', '1']) + for identifier, individual in context.population.items(): + self.assertTrue( + isinstance(individual, Individual), + "Each individual must be an instance of the Individual class" + ) + self.assertTrue( + isinstance(identifier, str), + "Each identifier must be a str" + ) + + def test___str__(self): + context = MemoryContext({'mybool': bool, 'myint': int}, ['0', '1']) + self.assertEqual( + str(context.population), + "['0', '1']", + "The string representation of the population is not correct" + ) + + +class MemoryModelTest(TestCase): + def test_context(self): + context = MemoryContext({'mybool': bool, 'myint': int}) + self.assertEqual( + context.model.context, + context, + "The context of the model must be the original context" + ) + + def test_population(self): + context = MemoryContext({'mybool': bool, 'myint': int}) + self.assertEqual( + context.model.population, + context.population, + "The population of the model must be the original population" + ) + + def test___getitem__(self): + context = MemoryContext({'mybool': bool, 'myint': int}) + self.assertEqual( + str(context.model['mybool']), + "{'name': 'mybool', 'type': }", + "The attribute mybool is not correct" + ) + + def test___setitem__(self): + context = MemoryContext( + {'mybool': bool, 'myint': int}, + {'0': {'mybool': True}, '1': {'myint': 1}} + ) + context.model['mybool'] = bool + self.assertEqual( + {identifier: str(individual) for identifier, individual in context.population.items()}, + {'0': "{'mybool': True, 'myint': 0}", '1': "{'mybool': False, 'myint': 1}"}, + "The individuals must remain unchanged" + ) + context.model['mybool'] = int + self.assertEqual( + {identifier: str(individual) for identifier, individual in context.population.items()}, + {'0': "{'mybool': 1, 'myint': 0}", '1': "{'mybool': 0, 'myint': 1}"}, + "The individuals must change" + ) + context.model['mybool'] = list + self.assertEqual( + {identifier: str(individual) for identifier, individual in context.population.items()}, + {'0': "{'mybool': [], 'myint': 0}", '1': "{'mybool': [], 'myint': 1}"}, + "The individuals must change" + ) + del context.model['myint'] + context.model['myint2'] = int + self.assertEqual( + {identifier: str(individual) for identifier, individual in context.population.items()}, + {'0': "{'mybool': [], 'myint2': 0}", '1': "{'mybool': [], 'myint2': 0}"}, + "The individuals must change" + ) + + def test___delitem__(self): + context = MemoryContext({'mybool': bool, 'myint': int}, ['0', '1']) + del context.model['mybool'] + self.assertEqual( + str(context), + "{'population': ['0', '1'], 'model': {'myint': }}", + "The context is not correct" + ) + self.assertEqual( + str(context.population['0']), + "{'myint': 0}", + "The context is not correct" + ) + + def test___len__(self): + context = MemoryContext({'mybool': bool, 'myint': int}) + self.assertEqual( + len(context.model), + 2, + "The length of the model is not correct" + ) + + def test___bool__(self): + context = MemoryContext({'mybool': bool, 'myint': int}) + self.assertTrue( + bool(context.model), + "The bool representation of the model is not correct" + ) + + def test___iter__(self): + context = MemoryContext({'mybool': bool, 'myint': int}) + self.assertEqual( + list(iter(context.model)), + ['mybool', 'myint'], + "The iteration over the model is not correct" + ) + + def test___str__(self): + context = MemoryContext({'mybool': bool, 'myint': int}) + self.assertEqual( + str(context.model), + "{'mybool': , 'myint': }", + "The string representation of the model must be correct" + ) + + +class MemoryIndividualTest(TestCase): + def test_identifier(self): + context = MemoryContext({'mybool': bool, 'myint': int}, ['0']) + self.assertEqual( + context.population['0'].identifier, + '0', + "The identifier must be '0'" + ) + + def test_population(self): + context = MemoryContext({'mybool': bool, 'myint': int}, ['0']) + self.assertEqual( + context.population['0'].population, + context.population, + "The population of the individual named '0' must be the context population" + ) + + def test_context(self): + context = MemoryContext({'mybool': bool, 'myint': int}, ['0']) + self.assertEqual( + context.population['0'].context, + context, + "The context of the individual named '0' must be the context" + ) + + def test_model(self): + context = MemoryContext({'mybool': bool, 'myint': int}, ['0']) + self.assertEqual( + context.population['0'].model, + context.model, + "The model of the individual named '0' must be the context model" + ) + + def test_value(self): + context = MemoryContext({'mybool': bool, 'myint': int}, ['0']) + self.assertEqual( + context.population['0'].value(context.model['mybool']), + bool(), + "The value 'mybool' of the individual named '0' must be the False" + ) + self.assertEqual( + context.population['0'].value(context.model['myint']), + int(), + "The value 'myint' of the individual named '0' must be the 0" + ) + other = MemoryContext({'mybool': bool, 'myint': int}, ['0']) + with self.assertRaises(ValueError): + _ = context.population['0'].value(other.model['myint']) + + def test___setitem__(self): + context = MemoryContext({'mybool': bool, 'myint': int}, ['0', '1']) + context.population['0']['mybool'] = True + context.population['0']['myint'] = 1 + self.assertEqual( + str(context.population['0']), + "{'mybool': True, 'myint': 1}", + "The textual representation of the first individual is not correct" + ) + context.population['1']['mybool'] = True + context.population['1']['myint'] = 2 + self.assertEqual( + str(context.population['1']), + "{'mybool': True, 'myint': 2}", + "The textual representation of the first individual is not correct" + ) + with self.assertRaises(KeyError): + context.population['0']['unknown'] = 1 + with self.assertRaises(ValueError): + context.population['0']['myint'] = 'abc' + + def test___getitem__(self): + context = MemoryContext({'mybool': bool, 'myint': int}, ['0']) + self.assertEqual( + context.population['0']['mybool'], + bool(), + "The first value is not correct" + ) + self.assertEqual( + context.population['0']['myint'], + int(), + "The second value is not correct" + ) + with self.assertRaises(KeyError): + _ = context.population['0']['unknown'] + + def test___delitem__(self): + context = MemoryContext({'mybool': bool, 'myint': int}, ['0']) + with self.assertRaises(ValueError): + del context.population['0']['myint'] + + def test___len__(self): + context = MemoryContext({'mybool': bool, 'myint': int}, ['0']) + self.assertEqual( + len(context.population['0']), + 2, + "The length of the individual is not correct" + ) + + def test___eq__(self): + context = MemoryContext({'mybool': bool, 'myint': int}, ['0', '1']) + self.assertTrue( + context.population['0'] == context.population['0'], + "The individual is equal to itself" + ) + self.assertFalse( + context.population['0'] == context.population['1'], + "Two different individuals are unequal" + ) + other = MemoryContext({'mybool': bool, 'myint': int}, ['0', '1']) + self.assertFalse( + context.population['0'] == other.population['0'], + "Two individuals from two different contexts are unequal" + ) + + def test___iter__(self): + context = MemoryContext({'mybool': bool, 'myint': int}, ['0', '1']) + self.assertEqual( + list(iter(context.population['0'])), + ['mybool', 'myint'], + "The iteration over the individual is not correct" + ) + + def test___str__(self): + context = MemoryContext({'mybool': bool, 'myint': int}, ['0', '1']) + self.assertEqual( + str(context.population['0']), + "{'mybool': False, 'myint': 0}", + "The string representation of the individual is not correct" + ) + + +class MemoryAttributeTest(TestCase): + def test_name(self): + context = MemoryContext({'mybool': bool, 'myint': int}, ['0']) + self.assertEqual( + context.model['mybool'].name, + 'mybool', + "The name must be 'mybool'" + ) + + def test_type(self): + context = MemoryContext({'mybool': bool, 'myint': int}, ['0']) + self.assertEqual( + context.model['mybool'].type, + bool, + "The type must be bool" + ) + + def test_value(self): + context = MemoryContext({'mybool': bool, 'myint': int}, ['0', '1']) + self.assertEqual( + context.model['mybool'].value(context.population['0']), + context.population['0'].value(context.model['mybool']), + "The value must be correct" + ) + + def test___getitem__(self): + context = MemoryContext({'mybool': bool, 'myint': int}, ['0', '1']) + self.assertEqual( + context.model['mybool']['0'], + context.population['0']['mybool'], + "The value must be correct" + ) + + def test___setitem__(self): + context = MemoryContext({'mybool': bool, 'myint': int}, ['0', '1']) + context.model['mybool']['0'] = True + context.model['myint']['0'] = 1 + self.assertEqual( + str(context.population['0']), + "{'mybool': True, 'myint': 1}", + "The textual representation of the first individual is not correct" + ) + context.model['mybool']['1'] = True + context.model['myint']['1'] = 2 + self.assertEqual( + str(context.population['1']), + "{'mybool': True, 'myint': 2}", + "The textual representation of the first individual is not correct" + ) + with self.assertRaises(KeyError): + context.model['mybool']['unknown'] = 1 + with self.assertRaises(ValueError): + context.model['myint']['0'] = 'abc' + + def test___len__(self): + context = MemoryContext({'mybool': bool, 'myint': int}, ['0', '1']) + self.assertEqual( + len(context.model['mybool']), + 2, + "The length must be correct" + ) + + def test___eq__(self): + context = MemoryContext({'mybool': bool, 'myint': int}, ['0', '1']) + self.assertTrue( + context.model['mybool'] == context.model['mybool'], + "The equality must be correct" + ) + + def test___iter__(self): + context = MemoryContext({'mybool': bool, 'myint': int}, ['0', '1']) + self.assertEqual( + list(iter(context.model['mybool'])), + ['0', '1'], + "The iter must be correct" + ) + + def test___str__(self): + context = MemoryContext({'mybool': bool, 'myint': int}, ['0', '1']) + self.assertEqual( + str(context.model['mybool']), + "{'name': 'mybool', 'type': }", + "The iter must be correct" + ) +