diff --git a/.github/requirements.txt b/.github/requirements.txt new file mode 100644 index 0000000..919974f --- /dev/null +++ b/.github/requirements.txt @@ -0,0 +1,6 @@ +PyYAML >=5.4 +distutils-pytest +ExifRead >= 2.2.0 +pytest >= 3.6.0 +pytest-dependency +setuptools_scm diff --git a/.github/workflows/run-tests.yaml b/.github/workflows/run-tests.yaml new file mode 100644 index 0000000..d81e2b1 --- /dev/null +++ b/.github/workflows/run-tests.yaml @@ -0,0 +1,32 @@ +name: Run Test +on: [push, pull_request] +jobs: + Test: + runs-on: ${{ matrix.os }} + strategy: + matrix: + python-version: + - '3.7' + - '3.8' + - '3.9' + - '3.10' + - '3.11' + os: [ubuntu-latest] + include: + - python-version: '3.6' + os: ubuntu-20.04 + steps: + - name: Check out repository code + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + pip install -r .github/requirements.txt + - name: Test with pytest + run: | + python setup.py test diff --git a/.gitignore b/.gitignore index 2389185..e4e2fda 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ -*.pyc -*~ __pycache__/ -/.cache/ +/.env /MANIFEST +/_meta.py /build/ /dist/ +/photoidx/__init__.py diff --git a/.req b/.req deleted file mode 100644 index e51be32..0000000 --- a/.req +++ /dev/null @@ -1,5 +0,0 @@ -PyYAML <= 5.2 ; python_version == '3.4' -PyYAML ; python_version > '3.4' -distutils-pytest -pytest >= 3.6.0 -pytest-dependency diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index b0f58c4..0000000 --- a/.travis.yml +++ /dev/null @@ -1,13 +0,0 @@ -language: python -python: - - "3.5" -before_install: - - sudo apt-get install -y libgexiv2-dev libyaml-dev python3-gi -virtualenv: - system_site_packages: true -install: pip install -r .req -script: make test - -# Local Variables: -# mode: yaml -# End: diff --git a/CHANGES b/CHANGES deleted file mode 100644 index 197c4ea..0000000 --- a/CHANGES +++ /dev/null @@ -1,210 +0,0 @@ - History of changes to photo-tools - ================================= - -* Version 0.9.3 (2020-05-03) - -** Bug fixes and minor changes - - + Issue #46: Fix YAMLLoadWarning. - -* Version 0.9.2 (2019-09-01) - -** Bug fixes and minor changes - - + Issue #45: update the code limiting the vignette thumbnailer - backends to use. - -* Version 0.9.1 (2019-08-21) - -** Bug fixes and minor changes - - + Issue #44: opening the filter options dialog fails with TypeError. - -* Version 0.9.0 (2019-08-05) - -** New features - - + Issue #39: Review behavior of imageview concerning writing the - index: the index is not automatically written to disk any more - after each modification, but the user need to explicitly save it. - imageview may create a new index if started with the --create - command line flag. - -** Incompatible changes - - + Drop support for Python 2. Require Python 3.4 or newer. - - + Use pathlib.Path rather then string in the IdxItem.filename - attribute. Switch to pathlib for most internal representation of - filesystem paths. As a side effect, the semantic of file paths may - be taken somewhat more coherent and strict now at some places. - -** Bug fixes and minor changes - - + Issue #42: imageview may inadvertently create an image index. - -* Version 0.8.2 (2019-01-01) - -** Bug fixes and minor changes - - + Issue #41: Setting filter options in imageviewer fails with - IndexError if current filter selects no image. - -* Version 0.8.1 (2019-01-01) - -** Bug fixes and minor changes - - + Issue #40: TypeError is raised when trying to read a non existing - index file. - -* Version 0.8 (2018-12-31) - -** New features - - + Issue #31: Implement modifying the current filter in imageview. - - + Issue #30: Protect the index file against conflicting concurrent - access using file system locking. - - + Issue #32: Add a stats command line interface subcommand. - - + Issue #20: Add a preferred order. Add actions to the GUI to push - images back and forth in the image order. - -** Incompatible changes - - + Issue #35: Change the sematic of the --date command line option to - photoidx.py and imageview.py: when an interval is given as - argument, the end time is taken exclusively. - E.g. --date=2015-03-14--2015-03-15 excludes images taken on - March 15. - -** Bug fixes and minor changes - - + Issue #36: Opening the overview images fails with IndexError if no - image is shown. - - + Issue #37: AttributeError is raised when when calling photoidx.py - without arguments. - - + Add a method Add method Index.extend_dir(). - - + Index.index() now supports the full variant having start and end - index arguments. - -* Version 0.7 (2017-12-31) - -** New features - - + Issue #21: Add more information to the info window. - - + Issue #27: Set default scale of imageview such that the first image - just fits the maximum window size. - -** Bug fixes and minor changes - - + Issue #28: use pytest-dependency to mark dependencies in the test - suite. - -* Version 0.6 (2017-05-22) - -** New features - - + Issue #24: Add an overview window. - -** Bug fixes and minor changes - - + Issue #25: imageview should remember rotation. - - + Issue #22: Unwanted unicode marker for tags in the index. - - + Issue #26: Get rid of PyGIWarning. - - + Add an optional attribute `name` to the items in the index. Use - it as the title of the imageview window if set. - -* Version 0.5 (2016-08-22) - -** New features - - + Issue #19: Manage a persistent selection - - + Issue #17: Speed up start of imageview when building in memory - index for many files. - - + Issue #18: Add an image info window in imageview. - - + Do not throw an error in imageview if an image cannot be read, - proceed to the next one instead. - -** Internal changes - - + Do not change directory when reading the image directory. - -* Version 0.4 (2016-04-12) - -** New features - - + Issue #4: Add option to photoidx to add missing images to an - index. - - + Issue #10: Allow setting of new tags from imageview. - - + Issue #11: imageview should be able to work without an index. - - + Issue #5: Allow a date interval as argument to --date. - - + Issue #12: Allow configuration of the type of checksum to be - calculated. - -** Incompatible changes - - + The index file format has changed. photoidx.py and imageview.py - are able to read the old format and convert the file silently to - the new format when writing it back. But the tools from earlier - versions will not fully work with the new format files. - -** Internal changes - - + Issue #1: Add a test suite. - - + Issue #3: Move from pyexiv2 to GExiv2. - -** Bug fixes and minor changes - - + Issue #6: imageview crashes with ZeroDivisionError if no tags are - set in the index. - - + Issue #13: imageview fails with RuntimeError if --directory option - is used. - - + Issue #15: photoidx.py create raises KeyError if exiftags are not - present in an image. - - + Issue #9: Sort the tags when writing the index to a file. - -* Version 0.3 (2016-01-02) - - + Add image viewer. - - + Add --date command line argument to select images. - - + Add command line arguments --gpspos and --gpsradius to select - images by GPS position. - - + Improve semantics in the --tags command line argument: Add - exclamation mark to negate tags and allow specifying an empty tag - list selecting only untagged images. - -* Version 0.2 (2015-10-21) - - + Add lstags sub command. - -* Version 0.1 (2015-09-19) - - + Initial version - - -# Local Variables: -# mode: org -# End: diff --git a/CHANGES.rst b/CHANGES.rst new file mode 100644 index 0000000..179bad4 --- /dev/null +++ b/CHANGES.rst @@ -0,0 +1,365 @@ +Changelog +========= + + +0.10.0 (2022-12-29) +~~~~~~~~~~~~~~~~~~~ + +New features +------------ + ++ `#43`_, `#57`_: Keep the center of current display stable, if + possible when zooming in or out in :ref:`imageview`. + +Incompatible changes +-------------------- + ++ `#52`_, `#53`_: Rename the package from ``photo`` to ``photoidx``. + +Internal changes +---------------- + ++ `#34`_, `#55`_: Upgrade to Pyside2. ++ `#48`_, `#56`_, `#58`_: Move from `gexiv2`_ to `ExifRead`_. ++ `#51`_: Review build tool chain. ++ `#47`_, `#50`_: Use `setuptools_scm`_ to manage the version number. + +Bug fixes and minor changes +--------------------------- + ++ `#54`_: Check whether vignette has any thumbnailer backend. ++ `#49`_: Fix :exc:`DeprecationWarning` about importing the ABCs from + :mod:`collections`. + +.. _#34: https://github.com/RKrahl/photoidx/issues/34 +.. _#43: https://github.com/RKrahl/photoidx/issues/43 +.. _#47: https://github.com/RKrahl/photoidx/issues/47 +.. _#48: https://github.com/RKrahl/photoidx/issues/48 +.. _#49: https://github.com/RKrahl/photoidx/pull/49 +.. _#50: https://github.com/RKrahl/photoidx/pull/50 +.. _#51: https://github.com/RKrahl/photoidx/pull/51 +.. _#52: https://github.com/RKrahl/photoidx/issues/52 +.. _#53: https://github.com/RKrahl/photoidx/pull/53 +.. _#54: https://github.com/RKrahl/photoidx/pull/54 +.. _#55: https://github.com/RKrahl/photoidx/pull/55 +.. _#56: https://github.com/RKrahl/photoidx/issues/56 +.. _#57: https://github.com/RKrahl/photoidx/pull/57 +.. _#58: https://github.com/RKrahl/photoidx/pull/58 + + +0.9.3 (2020-05-03) +~~~~~~~~~~~~~~~~~~ + +Bug fixes and minor changes +--------------------------- + ++ `#46`_: Fix :exc:`yaml.YAMLLoadWarning`. + +.. _#46: https://github.com/RKrahl/photoidx/issues/46 + + +0.9.2 (2019-09-01) +~~~~~~~~~~~~~~~~~~ + +Bug fixes and minor changes +--------------------------- + ++ `#45`_: update the code limiting the `vignette`_ thumbnailer + backends to use. + +.. _#45: https://github.com/RKrahl/photoidx/pull/45 + + +0.9.1 (2019-08-21) +~~~~~~~~~~~~~~~~~~ + +Bug fixes and minor changes +--------------------------- + ++ `#44`_: opening the filter options dialog fails with + :exc:`TypeError`. + +.. _#44: https://github.com/RKrahl/photoidx/issues/44 + + +0.9.0 (2019-08-05) +~~~~~~~~~~~~~~~~~~ + +New features +------------ + ++ `#39`_: Review behavior of :ref:`imageview` concerning writing the + index: the index is not automatically written to disk any more after + each modification, but the user need to explicitly save it. + :ref:`imageview` may create a new index if started with the + ``--create`` command line flag. + +Incompatible changes +-------------------- + ++ Drop support for Python 2. Require Python 3.4 or newer. + ++ Use :class:`pathlib.Path` rather then :class:`str` in + :attr:`photoidx.idxitem.IdxItem.filename`. Switch to :mod:`pathlib` + for most internal representation of filesystem paths. As a side + effect, the semantic of file paths may be taken somewhat more + coherent and strict now at some places. + +Bug fixes and minor changes +--------------------------- + ++ `#42`_: :ref:`imageview` may inadvertently create an image index. + +.. _#39: https://github.com/RKrahl/photoidx/issues/39 +.. _#42: https://github.com/RKrahl/photoidx/issues/42 + + +0.8.2 (2019-01-01) +~~~~~~~~~~~~~~~~~~ + +Bug fixes and minor changes +--------------------------- + ++ `#41`_: Setting filter options in + :class:`~photoidx.qt.imageViewer.ImageViewer` fails with + :exc:`IndexError` if current filter selects no image. + +.. _#41: https://github.com/RKrahl/photoidx/issues/41 + + +0.8.1 (2019-01-01) +~~~~~~~~~~~~~~~~~~ + +Bug fixes and minor changes +--------------------------- + ++ `#40`_: :exc:`TypeError` is raised when trying to read a non + existing index file. + +.. _#40: https://github.com/RKrahl/photoidx/issues/40 + + +0.8 (2018-12-31) +~~~~~~~~~~~~~~~~ + +New features +------------ + ++ `#31`_: Implement modifying the current filter in + :class:`~photoidx.qt.imageViewer.ImageViewer`. + ++ `#30`_: Protect the index file against conflicting concurrent access + using file system locking. + ++ `#32`_: Add a ``stats`` command line interface subcommand. + ++ `#20`_: Add a preferred order. Add actions to the GUI to push + images back and forth in the image order. + +Incompatible changes +-------------------- + ++ `#35`_: Change the sematic of the ``--date`` command line option to + :ref:`photo-idx` and :ref:`imageview`: when an interval is given as + argument, the end time is taken exclusively. + E.g. ``--date=2015-03-14--2015-03-15`` excludes images taken on + March 15. + +Bug fixes and minor changes +--------------------------- + ++ `#36`_: Opening the overview images fails with :exc:`IndexError` if + no image is shown. + ++ `#37`_: :exc:`AttributeError` is raised when calling :ref:`photo-idx` + without arguments. + ++ Add method :meth:`photoidx.index.Index.extend_dir`. + ++ :meth:`photoidx.index.Index.index` now supports the full variant + having start and end index arguments. + +.. _#20: https://github.com/RKrahl/photoidx/issues/20 +.. _#30: https://github.com/RKrahl/photoidx/issues/30 +.. _#31: https://github.com/RKrahl/photoidx/issues/31 +.. _#32: https://github.com/RKrahl/photoidx/issues/32 +.. _#35: https://github.com/RKrahl/photoidx/issues/35 +.. _#36: https://github.com/RKrahl/photoidx/issues/36 +.. _#37: https://github.com/RKrahl/photoidx/issues/37 + + +0.7 (2017-12-31) +~~~~~~~~~~~~~~~~ + +New features +------------ + ++ `#21`_: Add more information to the info window. + ++ `#27`_: Set default scale in + :class:`~photoidx.qt.imageViewer.ImageViewer` such that the first + image just fits the maximum window size. + +Bug fixes and minor changes +--------------------------- + ++ `#28`_: use `pytest-dependency`_ to mark dependencies in the test + suite. + +.. _#21: https://github.com/RKrahl/photoidx/issues/21 +.. _#27: https://github.com/RKrahl/photoidx/issues/27 +.. _#28: https://github.com/RKrahl/photoidx/issues/28 + + +0.6 (2017-05-22) +~~~~~~~~~~~~~~~~ + +New features +------------ + ++ `#24`_: Add an overview window. + +Bug fixes and minor changes +--------------------------- + ++ `#25`_: :class:`~photoidx.qt.imageViewer.ImageViewer` should + remember rotation. + ++ `#22`_: Unwanted unicode marker for tags in the index. + ++ `#26`_: Get rid of :exc:`gi.PyGIWarning`. + ++ Add an optional attribute :attr:`photoidx.idxitem.IdxItem.name`. Use + it as the title of the :class:`~photoidx.qt.imageViewer.ImageViewer` + window if set. + +.. _#22: https://github.com/RKrahl/photoidx/issues/22 +.. _#24: https://github.com/RKrahl/photoidx/issues/24 +.. _#25: https://github.com/RKrahl/photoidx/issues/25 +.. _#26: https://github.com/RKrahl/photoidx/issues/26 + + +0.5 (2016-08-22) +~~~~~~~~~~~~~~~~ + +New features +------------ + ++ `#19`_: Manage a persistent selection. + ++ `#17`_: Speed up start of :ref:`imageview` when building in memory + index for many files. + ++ `#18`_: Add an image info window in :ref:`imageview`. + ++ Do not throw an error in :ref:`imageview` if an image cannot be + read, proceed to the next one instead. + +Internal changes +---------------- + ++ Do not change directory when reading the image directory. + +.. _#17: https://github.com/RKrahl/photoidx/issues/17 +.. _#18: https://github.com/RKrahl/photoidx/issues/18 +.. _#19: https://github.com/RKrahl/photoidx/issues/19 + + +0.4 (2016-04-12) +~~~~~~~~~~~~~~~~ + +New features +------------ + ++ `#4`_: Add option to :ref:`photo-idx` to add missing images to an + index. + ++ `#10`_: Allow setting of new tags in :ref:`imageview`. + ++ `#11`_: :ref:`imageview` should be able to work without an index. + ++ `#5`_: Allow a date interval as argument to ``--date``. + ++ `#12`_: Allow configuration of the type of checksum to be + calculated. + +Incompatible changes +-------------------- + ++ The index file format has changed. :ref:`photo-idx` and + :ref:`imageview` are able to read the old format and convert the + file silently to the new format when writing it back. But the tools + from earlier versions will not fully work with the new format files. + +Internal changes +---------------- + ++ `#1`_: Add a test suite. + ++ `#3`_: Move from pyexiv2 to `gexiv2`_. + +Bug fixes and minor changes +--------------------------- + ++ `#6`_: :ref:`imageview` crashes with :exc:`ZeroDivisionError` if no + tags are set in the index. + ++ `#13`_: :ref:`imageview` fails with :exc:`RuntimeError` if + ``--directory`` option is used. + ++ `#15`_: :ref:`photo-idx` ``create`` raises :exc:`KeyError` if + exiftags are not present in an image. + ++ `#9`_: Sort the tags when writing the index to a file. + +.. _#1: https://github.com/RKrahl/photoidx/issues/1 +.. _#3: https://github.com/RKrahl/photoidx/issues/3 +.. _#4: https://github.com/RKrahl/photoidx/issues/4 +.. _#5: https://github.com/RKrahl/photoidx/issues/5 +.. _#6: https://github.com/RKrahl/photoidx/issues/6 +.. _#9: https://github.com/RKrahl/photoidx/issues/9 +.. _#10: https://github.com/RKrahl/photoidx/issues/10 +.. _#11: https://github.com/RKrahl/photoidx/issues/11 +.. _#12: https://github.com/RKrahl/photoidx/issues/12 +.. _#13: https://github.com/RKrahl/photoidx/issues/13 +.. _#15: https://github.com/RKrahl/photoidx/issues/15 + + +0.3 (2016-01-02) +~~~~~~~~~~~~~~~~ + +New features +------------ + ++ Add image viewer. + ++ Add ``--date`` command line argument to select images. + ++ Add command line arguments ``--gpspos`` and ``--gpsradius`` to + select images by GPS position. + ++ Improve semantics in the ``--tags`` command line argument: Add + exclamation mark to negate tags and allow specifying an empty tag + list selecting only untagged images. + + +0.2 (2015-10-21) +~~~~~~~~~~~~~~~~ + +New features +------------ + ++ Add ``lstags`` sub command. + + +0.1 (2015-09-19) +~~~~~~~~~~~~~~~~ + +Initial version + + +.. _ExifRead: https://github.com/ianare/exif-py +.. _setuptools_scm: https://github.com/pypa/setuptools_scm/ +.. _vignette: https://github.com/hydrargyrum/vignette +.. _pytest-dependency: https://github.com/RKrahl/pytest-dependency +.. _gexiv2: https://wiki.gnome.org/Projects/gexiv2 diff --git a/MANIFEST.in b/MANIFEST.in index 27f619c..09f4978 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,8 +1,10 @@ -include CHANGES +include CHANGES.rst include LICENSE.txt include MANIFEST.in include README.rst +include _meta.py include tests/conftest.py +include tests/pytest.ini include tests/test_*.py include tests/data/dsc_*.jpg include tests/data/index-*.yaml diff --git a/Makefile b/Makefile index b5b830f..2769ed0 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,5 @@ PYTHON = python3 +BUILDLIB = $(CURDIR)/build/lib build: @@ -11,15 +12,17 @@ sdist: $(PYTHON) setup.py sdist clean: - rm -f *~ photo/*~ photo/qt/*~ tests/*~ rm -rf build + rm -rf __pycache__ distclean: clean - rm -f MANIFEST - rm -f photo/*.pyc photo/qt/*.pyc tests/*.pyc - rm -rf .cache - rm -rf photo/__pycache__ photo/qt/__pycache__ tests/__pycache__ + rm -f MANIFEST _meta.py + rm -f photoidx/__init__.py rm -rf dist + rm -rf tests/.pytest_cache +meta: + $(PYTHON) setup.py meta -.PHONY: build test sdist clean distclean + +.PHONY: build test sdist clean distclean meta diff --git a/README.rst b/README.rst index ddd35df..46ec53f 100644 --- a/README.rst +++ b/README.rst @@ -1,13 +1,22 @@ -photo-tools - Tools for photo collections -========================================= +|gh-test| |pypi| -This package provides tools for the management of photo collections. -It maintains an index of the photos in a text file. All metadata is -stored in this index file in YAML format, the photos are accessed read -only. +.. |gh-test| image:: https://img.shields.io/github/actions/workflow/status/RKrahl/photoidx/run-tests.yaml?branch=develop + :target: https://github.com/RKrahl/photoidx/actions/workflows/run-tests.yaml + :alt: GitHub Workflow Status -The package provides a command line tool to manipulate the metadata -and a graphical image viewer. +.. |pypi| image:: https://img.shields.io/pypi/v/photoidx + :target: https://pypi.org/project/photoidx/ + :alt: PyPI version + +photoidx - Maintain indices for photo collections +================================================= + +This package maintains indices for photo collections. The index is +stored as a YAML file and contains metadata and tags describing the +photos. The photos are accessed read only. + +The package provides a command line tool to create and manipulate the +index and a graphical image viewer. System requirements @@ -15,15 +24,17 @@ System requirements Python: -+ Python 3.4 or newer. ++ Python 3.6 or newer. Required library packages: ++ `setuptools`_ + + `PyYAML`_ -+ `gexiv2`_ ++ `ExifRead`_ >= 2.2.0 -+ `PySide`_ ++ `PySide2`_ Optional library packages: @@ -33,7 +44,19 @@ Optional library packages: vignette is not available, everything will still work, but displaying the overview window may be significantly slower. -+ `pytest`_ ++ vignette needs at least one thumbnail backend, either `Pillow`_ or + `PyQt5`_. If no suitable backend is found, vignette will be + disabled in photoidx. + ++ `setuptools_scm`_ + + The version number is managed using this package. All source + distributions add a static text file with the version number and + fall back using that if `setuptools_scm` is not available. So this + package is only needed to build out of the plain development source + tree as cloned from GitHub. + ++ `pytest`_ >= 3.0.0 Only needed to run the test suite. @@ -48,13 +71,36 @@ Optional library packages: Only needed to run the test suite. -Installation ------------- +Install instructions +-------------------- + +Note that the GUI of photoidx requires PySide2, but the installation +of PySide2 using pip seem to be thoroughly broken. That is why that +dependency is deliberately omitted in the setup script of photoidx. +You need to install PySide2 independently before installing photoidx. +It is advisable to install PySide2 using the package manager of your +operating system rather than from PyPI. -This package uses the distutils Python standard library package and -follows its conventions of packaging source distributions. See the -documentation on `Installing Python Modules`_ for details or to -customize the install process. +Furthermore, you may want to install vignette along with a thumbnail +backend to enable cached thumbnails in the overview window. This also +needs to be installed independently. + +Release packages of photoidx are published in the `Python Package +Index (PyPI)`__. + +.. __: `PyPI site`_ + +Installation using pip +...................... + +You can install photoidx from PyPI using pip:: + + $ pip install photoidx + +Installation from the source distribution +......................................... + +Steps to manually build from the source distribution: 1. Download the sources, unpack, and change into the source directory. @@ -73,14 +119,19 @@ customize the install process. The last step might require admin privileges in order to write into the site-packages directory of your Python installation. +Note that this still requires a release version of the source +distribution. The development sources that you may clone from the +source repository on GitHub is missing some files that are dynamically +created during the release. + Copyright and License --------------------- -Copyright 2015–2020 Rolf Krahl +Copyright 2015–2022 Rolf Krahl Licensed under the `Apache License`_, Version 2.0 (the "License"); you -may not use this file except in compliance with the License. +may not use this package except in compliance with the License. Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -89,12 +140,16 @@ implied. See the License for the specific language governing permissions and limitations under the License. +.. _setuptools: https://github.com/pypa/setuptools/ .. _PyYAML: https://github.com/yaml/pyyaml -.. _gexiv2: https://wiki.gnome.org/Projects/gexiv2 -.. _PySide: https://wiki.qt.io/PySide +.. _ExifRead: https://github.com/ianare/exif-py +.. _PySide2: https://www.pyside.org/ .. _vignette: https://github.com/hydrargyrum/vignette +.. _Pillow: https://python-pillow.org/ +.. _PyQt5: https://www.riverbankcomputing.com/software/pyqt/ +.. _setuptools_scm: https://github.com/pypa/setuptools_scm/ .. _pytest: https://pytest.org/ .. _pytest-dependency: https://github.com/RKrahl/pytest-dependency .. _distutils-pytest: https://github.com/RKrahl/distutils-pytest -.. _Installing Python Modules: https://docs.python.org/3/install/ +.. _PyPI site: https://pypi.org/project/photoidx/ .. _Apache License: https://www.apache.org/licenses/LICENSE-2.0 diff --git a/attic/README.rst b/attic/README.rst new file mode 100644 index 0000000..268f552 --- /dev/null +++ b/attic/README.rst @@ -0,0 +1,9 @@ +Attic +===== + +This folder contains an unsorted collection of tools, modules and +script fragments that are linked in some way with photoidx, but are +not part of the distribution. That might include ideas that I started +once but never finalized to a point to formally include them into +photoidx or scripts that may be handly for a particular use case but +that are too specific to be included. diff --git a/attic/partition.py b/attic/partition.py new file mode 100755 index 0000000..057131f --- /dev/null +++ b/attic/partition.py @@ -0,0 +1,58 @@ +#! /usr/bin/python3 +"""Try to find a partition of tags for a photo index. + +Here, a partition of tags is defined as a set of tags such that each +item in the index is tagged with one and only one tag out of that +partition. + +Note that such a partition may not exist for an index. Furthermore, +the solution might not be unique, e.g. more then one partition may +exist for the same index. +""" + +import photoidx.index + + +def tag_index(idx): + """Return a mapping of tag names to index items. + """ + tagidx = dict() + for i in idx: + for t in i.tags: + if t not in tagidx: + tagidx[t] = set() + tagidx[t].add(i) + return tagidx + +def get_partition(idx): + """Try to find a partition of tags for the index. + + The implementation follows an heuristic approach that is not + guaranteed to find a solution, even if one exists. + """ + tagidx = tag_index(idx) + taglist = sorted(tagidx.keys(), key=lambda t: len(tagidx[t]), reverse=True) + partition = set() + covered = set() + for t in taglist: + if tagidx[t] & covered: + continue + partition.add(t) + covered |= tagidx[t] + if len(covered) == len(idx): + # Found a solution + return partition + else: + # Failed + return None + +if __name__ == "__main__": + import argparse + argparser = argparse.ArgumentParser(description=__doc__.split("\n")[0]) + argparser.add_argument('-d', '--directory', + help="image directory", default=".") + args = argparser.parse_args() + with photoidx.index.Index(idxfile=args.directory) as idx: + partition = get_partition(idx) + if partition: + print("\n".join(partition)) diff --git a/photo/__init__.py b/photo/__init__.py deleted file mode 100644 index 330eac8..0000000 --- a/photo/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -"""Tools for photo collections. - -This package provides tools for managing photo collections. -""" - -__version__ = "0.9.3" -__author__ = "Rolf Krahl " diff --git a/photo/qt/__init__.py b/photo/qt/__init__.py deleted file mode 100644 index c0c645b..0000000 --- a/photo/qt/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -"""GUI elements based on PySide. -""" - -from photo.qt.imageViewer import ImageViewer diff --git a/photo/exif.py b/photoidx/exif.py similarity index 57% rename from photo/exif.py rename to photoidx/exif.py index 8a61751..094e525 100644 --- a/photo/exif.py +++ b/photoidx/exif.py @@ -1,13 +1,10 @@ """Wrapper around the exif library to extract and convert some information. """ +import datetime import fractions import warnings -with warnings.catch_warnings(): - # Issue #26 - warnings.simplefilter("ignore") - from gi.repository import GExiv2 - +import exifread # Some helper classes for exif attributes, having customized string # representations. @@ -49,75 +46,87 @@ class Exif(object): 8: 'Rotate 270 CW', } - def __init__(self, filename): - self._exif = GExiv2.Metadata(str(filename)) + def __init__(self, path): + with path.open("rb") as f: + self._tags = exifread.process_file(f) @property def createDate(self): """Time and date the image was taken.""" try: - return self._exif.get_date_time() - except KeyError: + dt = self._tags['Image DateTime'].values + except (AttributeError, KeyError): return None + else: + return datetime.datetime.strptime(dt, "%Y:%m:%d %H:%M:%S") @property def orientation(self): """Orientation of the camera relative to the scene.""" try: - orientation = self._exif.get_orientation() - return self.OrientationXlate[int(orientation)] - except KeyError: + orientation = self._tags['Image Orientation'].values[0] + except (AttributeError, KeyError): return None + else: + return self.OrientationXlate[int(orientation)] @property def gpsPosition(self): """GPS coordinates.""" - if ('Exif.GPSInfo.GPSLatitude' in self._exif and - 'Exif.GPSInfo.GPSLongitude' in self._exif): - lat = self._exif.get_gps_latitude() - lon = self._exif.get_gps_longitude() - latref = 'N' if lat >= 0.0 else 'S' - lonref = 'E' if lon >= 0.0 else 'W' - return { latref:abs(lat), lonref:abs(lon) } - else: + try: + lat_tuple = self._tags['GPS GPSLatitude'].values + lon_tuple = self._tags['GPS GPSLongitude'].values + latref = self._tags['GPS GPSLatitudeRef'].values + lonref = self._tags['GPS GPSLongitudeRef'].values + except (AttributeError, KeyError): return None + else: + lat = lat_tuple[0] + lat_tuple[1]/60 + lat_tuple[2]/3600 + lon = lon_tuple[0] + lon_tuple[1]/60 + lon_tuple[2]/3600 + return { latref:float(lat), lonref:float(lon) } @property def cameraModel(self): """Camera Model.""" try: - return self._exif['Exif.Image.Model'] - except KeyError: + return self._tags['Image Model'].values + except (AttributeError, KeyError): return None @property def exposureTime(self): """Exposure time.""" try: - return ExposureTime(self._exif.get_exposure_time()) - except KeyError: + et = self._tags['EXIF ExposureTime'].values[0] + except (AttributeError, KeyError): return None + else: + return ExposureTime(et) @property def aperture(self): """Aperture.""" try: - return Aperture(self._exif.get_fnumber()) - except KeyError: + f = self._tags['EXIF FNumber'].values[0] + except (AttributeError, KeyError): return None + else: + return Aperture(f) @property def iso(self): """ISO speed rating.""" try: - return self._exif['Exif.Photo.ISOSpeedRatings'] - except KeyError: + return self._tags['EXIF ISOSpeedRatings'].values[0] + except (AttributeError, KeyError): return None @property def focalLength(self): """Lens focal length.""" try: - return FocalLength(self._exif.get_focal_length()) - except KeyError: + fl = self._tags['EXIF FocalLength'].values[0] + except (AttributeError, KeyError): return None + else: + return FocalLength(fl) diff --git a/photo/geo.py b/photoidx/geo.py similarity index 99% rename from photo/geo.py rename to photoidx/geo.py index 64bae64..28eb09f 100644 --- a/photo/geo.py +++ b/photoidx/geo.py @@ -3,7 +3,7 @@ import re import math -from collections import Mapping +from collections.abc import Mapping _geopos_pattern = (r"^\s*(?P\d+(?:\.\d*))\s*(?PN|S),\s*" diff --git a/photo/idxfilter.py b/photoidx/idxfilter.py similarity index 98% rename from photo/idxfilter.py rename to photoidx/idxfilter.py index 727549d..7cef022 100644 --- a/photo/idxfilter.py +++ b/photoidx/idxfilter.py @@ -2,11 +2,10 @@ """ import argparse -import collections import datetime from pathlib import Path import re -from photo.geo import GeoPosition +from photoidx.geo import GeoPosition _datere = re.compile(r'''^ diff --git a/photo/idxitem.py b/photoidx/idxitem.py similarity index 97% rename from photo/idxitem.py rename to photoidx/idxitem.py index d8e19c8..86bc8fe 100644 --- a/photo/idxitem.py +++ b/photoidx/idxitem.py @@ -3,8 +3,8 @@ import hashlib from pathlib import Path -from photo.exif import Exif -from photo.geo import GeoPosition +from photoidx.exif import Exif +from photoidx.geo import GeoPosition def _checksum(fname, hashalg): diff --git a/photo/index.py b/photoidx/index.py similarity index 96% rename from photo/index.py rename to photoidx/index.py index d515531..f96cf3f 100644 --- a/photo/index.py +++ b/photoidx/index.py @@ -1,14 +1,14 @@ """Provide the class Index which represents an index of photos. """ -from collections import MutableSequence +from collections.abc import MutableSequence import errno import fcntl import os from pathlib import Path import yaml -from photo.idxitem import IdxItem -from photo.listtools import LazyList +from photoidx.idxitem import IdxItem +from photoidx.listtools import LazyList class AlreadyLockedError(OSError): diff --git a/photo/listtools.py b/photoidx/listtools.py similarity index 93% rename from photo/listtools.py rename to photoidx/listtools.py index 68b60e9..f906abe 100644 --- a/photo/listtools.py +++ b/photoidx/listtools.py @@ -1,14 +1,13 @@ """Some useful list classes. -**Note**: This module might be useful independently of photo-tools. -It is included here because photo-tools uses it internally, but it is -not considered to be part of the API. Changes in this module are not -considered API changes of photo-tools. It may even be removed from -future versions of the photo-tools distribution without further -notice. +**Note**: This module might be useful independently of photoidx. It +is included here because photoidx uses it internally, but it is not +considered to be part of the API. Changes in this module are not +considered API changes of photoidx. It may even be removed from +future versions of the photoidx distribution without further notice. """ -from collections import MutableSequence +from collections.abc import MutableSequence class LazyList(MutableSequence): """A list generated lazily from an iterable. diff --git a/photoidx/qt/__init__.py b/photoidx/qt/__init__.py new file mode 100644 index 0000000..539f1c1 --- /dev/null +++ b/photoidx/qt/__init__.py @@ -0,0 +1,4 @@ +"""GUI elements based on PySide. +""" + +from photoidx.qt.imageViewer import ImageViewer diff --git a/photo/qt/filterDialog.py b/photoidx/qt/filterDialog.py similarity index 81% rename from photo/qt/filterDialog.py rename to photoidx/qt/filterDialog.py index fc61f7b..46d8840 100644 --- a/photo/qt/filterDialog.py +++ b/photoidx/qt/filterDialog.py @@ -2,12 +2,12 @@ """ import datetime -from PySide import QtCore, QtGui -import photo.idxfilter -from photo.geo import GeoPosition +from PySide2 import QtCore, QtWidgets +import photoidx.idxfilter +from photoidx.geo import GeoPosition -class GeoPosEdit(QtGui.QLineEdit): +class GeoPosEdit(QtWidgets.QLineEdit): """A QLineEdit with a suitable size for a GeoPosition. """ def sizeHint(self): @@ -21,7 +21,7 @@ def sizeHint(self): class FilterOption(object): def __init__(self, criterion, parent): - self.groupbox = QtGui.QGroupBox("Filter by %s" % criterion) + self.groupbox = QtWidgets.QGroupBox("Filter by %s" % criterion) self.groupbox.setCheckable(True) parent.addWidget(self.groupbox) @@ -35,10 +35,10 @@ class TagFilterOption(FilterOption): def __init__(self, parent): super().__init__("tags", parent) - self.entry = QtGui.QLineEdit() - label = QtGui.QLabel("Tags:") + self.entry = QtWidgets.QLineEdit() + label = QtWidgets.QLabel("Tags:") label.setBuddy(self.entry) - layout = QtGui.QHBoxLayout() + layout = QtWidgets.QHBoxLayout() layout.addWidget(label) layout.addWidget(self.entry) self.groupbox.setLayout(layout) @@ -60,9 +60,9 @@ class SelectFilterOption(FilterOption): def __init__(self, parent): super().__init__("selection", parent) - self.buttonYes = QtGui.QRadioButton("selected") - self.buttonNo = QtGui.QRadioButton("not selected") - layout = QtGui.QHBoxLayout() + self.buttonYes = QtWidgets.QRadioButton("selected") + self.buttonNo = QtWidgets.QRadioButton("not selected") + layout = QtWidgets.QHBoxLayout() layout.addWidget(self.buttonYes) layout.addWidget(self.buttonNo) self.groupbox.setLayout(layout) @@ -87,13 +87,13 @@ class DateFilterOption(FilterOption): def __init__(self, parent): super().__init__("date", parent) - self.startEntry = QtGui.QLineEdit() - startLabel = QtGui.QLabel("Start:") + self.startEntry = QtWidgets.QLineEdit() + startLabel = QtWidgets.QLabel("Start:") startLabel.setBuddy(self.startEntry) - self.endEntry = QtGui.QLineEdit() - endLabel = QtGui.QLabel("End:") + self.endEntry = QtWidgets.QLineEdit() + endLabel = QtWidgets.QLabel("End:") endLabel.setBuddy(self.endEntry) - layout = QtGui.QGridLayout() + layout = QtWidgets.QGridLayout() layout.addWidget(startLabel, 0, 0) layout.addWidget(self.startEntry, 0, 1) layout.addWidget(endLabel, 1, 0) @@ -108,7 +108,7 @@ def getOption(self): datestr = "%s--%s" % (startdate, enddate) else: datestr = startdate - return { 'date': photo.idxfilter.strpdate(datestr) } + return { 'date': photoidx.idxfilter.strpdate(datestr) } else: return {} @@ -123,12 +123,12 @@ class GPSFilterOption(FilterOption): def __init__(self, parent): super().__init__("GPS position", parent) self.posEntry = GeoPosEdit() - posLabel = QtGui.QLabel("Position:") + posLabel = QtWidgets.QLabel("Position:") posLabel.setBuddy(self.posEntry) - self.radiusEntry = QtGui.QLineEdit() - radiusLabel = QtGui.QLabel("Radius:") + self.radiusEntry = QtWidgets.QLineEdit() + radiusLabel = QtWidgets.QLabel("Radius:") radiusLabel.setBuddy(self.radiusEntry) - layout = QtGui.QGridLayout() + layout = QtWidgets.QGridLayout() layout.addWidget(posLabel, 0, 0) layout.addWidget(self.posEntry, 0, 1) layout.addWidget(radiusLabel, 1, 0) @@ -152,10 +152,10 @@ class ListFilterOption(FilterOption): def __init__(self, parent): super().__init__("explicit file names", parent) - self.entry = QtGui.QLineEdit() - label = QtGui.QLabel("Files:") + self.entry = QtWidgets.QLineEdit() + label = QtWidgets.QLabel("Files:") label.setBuddy(self.entry) - layout = QtGui.QHBoxLayout() + layout = QtWidgets.QHBoxLayout() layout.addWidget(label) layout.addWidget(self.entry) self.groupbox.setLayout(layout) @@ -172,12 +172,12 @@ def setOption(self, filelist): self.entry.setText(" ".join(sorted(str(p) for p in filelist))) -class FilterDialog(QtGui.QDialog): +class FilterDialog(QtWidgets.QDialog): def __init__(self): super().__init__() - mainLayout = QtGui.QVBoxLayout() + mainLayout = QtWidgets.QVBoxLayout() self.tagFilterOption = TagFilterOption(mainLayout) self.selectFilterOption = SelectFilterOption(mainLayout) @@ -185,8 +185,8 @@ def __init__(self): self.gpsFilterOption = GPSFilterOption(mainLayout) self.filelistFilterOption = ListFilterOption(mainLayout) - buttonBox = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Ok | - QtGui.QDialogButtonBox.Cancel) + btn = QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel + buttonBox = QtWidgets.QDialogButtonBox(btn) buttonBox.accepted.connect(self.accept) buttonBox.rejected.connect(self.reject) mainLayout.addWidget(buttonBox, alignment=QtCore.Qt.AlignHCenter) @@ -209,5 +209,5 @@ def accept(self): filterArgs.update(self.dateFilterOption.getOption()) filterArgs.update(self.gpsFilterOption.getOption()) filterArgs.update(self.filelistFilterOption.getOption()) - self.imgFilter = photo.idxfilter.IdxFilter(**filterArgs) + self.imgFilter = photoidx.idxfilter.IdxFilter(**filterArgs) super().accept() diff --git a/photo/qt/image.py b/photoidx/qt/image.py similarity index 88% rename from photo/qt/image.py rename to photoidx/qt/image.py index 2094444..9035dce 100644 --- a/photo/qt/image.py +++ b/photoidx/qt/image.py @@ -1,13 +1,16 @@ """Provide the class Image corresponding to an IdxItem. """ +import logging import re -from PySide import QtCore, QtGui +from PySide2 import QtCore, QtGui, QtWidgets try: import vignette except ImportError: vignette = None +log = logging.getLogger(__name__) + # Limit the vignette thumbnailer backends to those dealing with images. if vignette: try: @@ -24,6 +27,11 @@ else: # vignette is too old to be usable vignette = None + if vignette: + if not list(vignette.iter_thumbnail_backends()): + log.warning("Disabling vignette: " + "no suitable thumbnailer backend available") + vignette = None class ImageNotFoundError(Exception): diff --git a/photo/qt/imageInfoDialog.py b/photoidx/qt/imageInfoDialog.py similarity index 70% rename from photo/qt/imageInfoDialog.py rename to photoidx/qt/imageInfoDialog.py index 7f11d10..6fa5457 100644 --- a/photo/qt/imageInfoDialog.py +++ b/photoidx/qt/imageInfoDialog.py @@ -1,68 +1,68 @@ """A dialog window to show some informations on the current image. """ -from PySide import QtCore, QtGui -from photo.exif import Exif +from PySide2 import QtCore, QtWidgets +from photoidx.exif import Exif -class ImageInfoDialog(QtGui.QDialog): +class ImageInfoDialog(QtWidgets.QDialog): def __init__(self, basedir): super().__init__() self.basedir = basedir - infoLayout = QtGui.QGridLayout() - cameraModelLabel = QtGui.QLabel("Camera model:") - self.cameraModel = QtGui.QLabel() + infoLayout = QtWidgets.QGridLayout() + cameraModelLabel = QtWidgets.QLabel("Camera model:") + self.cameraModel = QtWidgets.QLabel() self.cameraModel.setTextFormat(QtCore.Qt.PlainText) infoLayout.addWidget(cameraModelLabel, 0, 0) infoLayout.addWidget(self.cameraModel, 0, 1) - filenameLabel = QtGui.QLabel("File name:") - self.filename = QtGui.QLabel() + filenameLabel = QtWidgets.QLabel("File name:") + self.filename = QtWidgets.QLabel() self.filename.setTextFormat(QtCore.Qt.PlainText) infoLayout.addWidget(filenameLabel, 1, 0) infoLayout.addWidget(self.filename, 1, 1) - createDateLabel = QtGui.QLabel("Create date:") - self.createDate = QtGui.QLabel() + createDateLabel = QtWidgets.QLabel("Create date:") + self.createDate = QtWidgets.QLabel() self.createDate.setTextFormat(QtCore.Qt.PlainText) infoLayout.addWidget(createDateLabel, 2, 0) infoLayout.addWidget(self.createDate, 2, 1) - orientationLabel = QtGui.QLabel("Orientation:") - self.orientation = QtGui.QLabel() + orientationLabel = QtWidgets.QLabel("Orientation:") + self.orientation = QtWidgets.QLabel() self.orientation.setTextFormat(QtCore.Qt.PlainText) infoLayout.addWidget(orientationLabel, 3, 0) infoLayout.addWidget(self.orientation, 3, 1) - gpsPositionLabel = QtGui.QLabel("GPS position:") - self.gpsPosition = QtGui.QLabel() + gpsPositionLabel = QtWidgets.QLabel("GPS position:") + self.gpsPosition = QtWidgets.QLabel() self.gpsPosition.setTextFormat(QtCore.Qt.RichText) self.gpsPosition.setOpenExternalLinks(True) infoLayout.addWidget(gpsPositionLabel, 4, 0) infoLayout.addWidget(self.gpsPosition, 4, 1) - exposureTimeLabel = QtGui.QLabel("Exposure time:") - self.exposureTime = QtGui.QLabel() + exposureTimeLabel = QtWidgets.QLabel("Exposure time:") + self.exposureTime = QtWidgets.QLabel() self.exposureTime.setTextFormat(QtCore.Qt.PlainText) infoLayout.addWidget(exposureTimeLabel, 5, 0) infoLayout.addWidget(self.exposureTime, 5, 1) - apertureLabel = QtGui.QLabel("F-number:") - self.aperture = QtGui.QLabel() + apertureLabel = QtWidgets.QLabel("F-number:") + self.aperture = QtWidgets.QLabel() self.aperture.setTextFormat(QtCore.Qt.PlainText) infoLayout.addWidget(apertureLabel, 6, 0) infoLayout.addWidget(self.aperture, 6, 1) - isoLabel = QtGui.QLabel("ISO speed rating:") - self.iso = QtGui.QLabel() + isoLabel = QtWidgets.QLabel("ISO speed rating:") + self.iso = QtWidgets.QLabel() self.iso.setTextFormat(QtCore.Qt.PlainText) infoLayout.addWidget(isoLabel, 7, 0) infoLayout.addWidget(self.iso, 7, 1) - focalLengthLabel = QtGui.QLabel("Focal length:") - self.focalLength = QtGui.QLabel() + focalLengthLabel = QtWidgets.QLabel("Focal length:") + self.focalLength = QtWidgets.QLabel() self.focalLength.setTextFormat(QtCore.Qt.PlainText) infoLayout.addWidget(focalLengthLabel, 8, 0) infoLayout.addWidget(self.focalLength, 8, 1) - buttonBox = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Ok) + buttonBox = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Ok) buttonBox.accepted.connect(self.accept) - mainLayout = QtGui.QVBoxLayout() + mainLayout = QtWidgets.QVBoxLayout() mainLayout.addLayout(infoLayout) mainLayout.addWidget(buttonBox, alignment=QtCore.Qt.AlignHCenter) self.setLayout(mainLayout) diff --git a/photo/qt/imageViewer.py b/photoidx/qt/imageViewer.py similarity index 62% rename from photo/qt/imageViewer.py rename to photoidx/qt/imageViewer.py index 3b5ab2d..025fed9 100644 --- a/photo/qt/imageViewer.py +++ b/photoidx/qt/imageViewer.py @@ -2,17 +2,17 @@ """ import sys -from PySide import QtCore, QtGui -import photo.index -from photo.listtools import LazyList -from photo.qt.image import Image -from photo.qt.filterDialog import FilterDialog -from photo.qt.imageInfoDialog import ImageInfoDialog -from photo.qt.overviewWindow import OverviewWindow -from photo.qt.tagSelectDialog import TagSelectDialog +from PySide2 import QtCore, QtGui, QtWidgets +import photoidx.index +from photoidx.listtools import LazyList +from photoidx.qt.image import Image +from photoidx.qt.filterDialog import FilterDialog +from photoidx.qt.imageInfoDialog import ImageInfoDialog +from photoidx.qt.overviewWindow import OverviewWindow +from photoidx.qt.tagSelectDialog import TagSelectDialog -class ImageViewer(QtGui.QMainWindow): +class ImageViewer(QtWidgets.QMainWindow): def __init__(self, images, imgFilter, scaleFactor=1.0, readOnly=False, dirty=False): @@ -41,97 +41,115 @@ def __init__(self, images, imgFilter, self.filterDialog = FilterDialog() - self.imageLabel = QtGui.QLabel() - self.imageLabel.setSizePolicy(QtGui.QSizePolicy.Ignored, - QtGui.QSizePolicy.Ignored) + self.imageLabel = QtWidgets.QLabel() + self.imageLabel.setSizePolicy(QtWidgets.QSizePolicy.Ignored, + QtWidgets.QSizePolicy.Ignored) self.imageLabel.setScaledContents(True) - self.scrollArea = QtGui.QScrollArea() + self.scrollArea = QtWidgets.QScrollArea() self.scrollArea.setBackgroundRole(QtGui.QPalette.Dark) self.scrollArea.setWidget(self.imageLabel) self.scrollArea.setAlignment(QtCore.Qt.AlignCenter) self.setCentralWidget(self.scrollArea) - maxSize = 0.95 * QtGui.QApplication.desktop().screenGeometry().size() + maxSize = 0.95 * QtWidgets.QApplication.desktop().screenGeometry().size() self.setMaximumSize(maxSize) - self.saveAct = QtGui.QAction("&Save index", self, - shortcut="Ctrl+s", enabled=(not self.readOnly), - triggered=self.saveIndex) - self.closeAct = QtGui.QAction("&Close", self, - shortcut="q", triggered=self.close) - self.filterOptsAct = QtGui.QAction("Filter Options", self, - shortcut="Shift+Ctrl+f", triggered=self.filterOptions) - self.zoomInAct = QtGui.QAction("Zoom &In", self, - shortcut=">", triggered=self.zoomIn) - self.zoomOutAct = QtGui.QAction("Zoom &Out", self, - shortcut="<", triggered=self.zoomOut) - self.zoomFitHeightAct = QtGui.QAction("Zoom to Fit &Height", self, - triggered=self.zoomFitHeight) - self.zoomFitWidthAct = QtGui.QAction("Zoom to Fit &Width", self, - triggered=self.zoomFitWidth) - self.rotateLeftAct = QtGui.QAction("Rotate &Left", self, - shortcut="l", triggered=self.rotateLeft) - self.rotateRightAct = QtGui.QAction("Rotate &Right", self, - shortcut="r", triggered=self.rotateRight) - self.fullScreenAct = QtGui.QAction("Show &Full Screen", self, - shortcut="f", checkable=True, triggered=self.fullScreen) - self.imageInfoAct = QtGui.QAction("Image &Info", self, - shortcut="i", triggered=self.imageInfo) - self.overviewAct = QtGui.QAction("&Overview Window", self, - shortcut="o", triggered=self.overview) - self.prevImageAct = QtGui.QAction("&Previous Image", self, - shortcut="p", enabled=False, triggered=self.prevImage) - self.nextImageAct = QtGui.QAction("&Next Image", self, - shortcut="n", enabled=(self._haveNext()), - triggered=self.nextImage) - self.selectImageAct = QtGui.QAction("&Select Image", self, - shortcut="s", enabled=(not self.readOnly), - triggered=self.selectImage) - self.deselectImageAct = QtGui.QAction("&Deselect Image", self, - shortcut="d", enabled=(not self.readOnly), - triggered=self.deselectImage) - self.pushForwardAct = QtGui.QAction("Push Image &Forward", self, - shortcut="Ctrl+f", enabled=(not self.readOnly), - triggered=self.pushImageForward) - self.pushBackwardAct = QtGui.QAction("Push Image &Backward", self, - shortcut="Ctrl+b", enabled=(not self.readOnly), - triggered=self.pushImageBackward) - self.tagSelectAct = QtGui.QAction("&Tags", self, - shortcut="t", enabled=(not self.readOnly), - triggered=self.tagSelect) - - self.fileMenu = QtGui.QMenu("&File", self) - self.fileMenu.addAction(self.saveAct) - self.fileMenu.addAction(self.closeAct) - self.fileMenu.addAction(self.filterOptsAct) - self.menuBar().addMenu(self.fileMenu) - - self.viewMenu = QtGui.QMenu("&View", self) - self.viewMenu.addAction(self.zoomInAct) - self.viewMenu.addAction(self.zoomOutAct) - self.viewMenu.addAction(self.zoomFitHeightAct) - self.viewMenu.addAction(self.zoomFitWidthAct) - self.viewMenu.addAction(self.rotateLeftAct) - self.viewMenu.addAction(self.rotateRightAct) - self.viewMenu.addSeparator() - self.viewMenu.addAction(self.fullScreenAct) - self.viewMenu.addSeparator() - self.viewMenu.addAction(self.imageInfoAct) - self.viewMenu.addAction(self.overviewAct) - self.menuBar().addMenu(self.viewMenu) - - self.imageMenu = QtGui.QMenu("&Image", self) - self.imageMenu.addAction(self.prevImageAct) - self.imageMenu.addAction(self.nextImageAct) - self.imageMenu.addAction(self.selectImageAct) - self.imageMenu.addAction(self.deselectImageAct) - self.imageMenu.addSeparator() - self.imageMenu.addAction(self.pushForwardAct) - self.imageMenu.addAction(self.pushBackwardAct) - self.imageMenu.addSeparator() - self.imageMenu.addAction(self.tagSelectAct) - self.menuBar().addMenu(self.imageMenu) + self.saveAct = QtWidgets.QAction("&Save index", self) + self.saveAct.setShortcut("Ctrl+s") + self.saveAct.setEnabled(not self.readOnly) + self.saveAct.triggered.connect(self.saveIndex) + self.closeAct = QtWidgets.QAction("&Close", self) + self.closeAct.setShortcut("q") + self.closeAct.triggered.connect(self.close) + self.filterOptsAct = QtWidgets.QAction("Filter Options", self) + self.filterOptsAct.setShortcut("Shift+Ctrl+f") + self.filterOptsAct.triggered.connect(self.filterOptions) + self.zoomInAct = QtWidgets.QAction("Zoom &In", self) + self.zoomInAct.setShortcut(">") + self.zoomInAct.triggered.connect(self.zoomIn) + self.zoomOutAct = QtWidgets.QAction("Zoom &Out", self) + self.zoomOutAct.setShortcut("<") + self.zoomOutAct.triggered.connect(self.zoomOut) + self.zoomFitHeightAct = QtWidgets.QAction("Zoom to Fit &Height", self) + self.zoomFitHeightAct.triggered.connect(self.zoomFitHeight) + self.zoomFitWidthAct = QtWidgets.QAction("Zoom to Fit &Width", self) + self.zoomFitWidthAct.triggered.connect(self.zoomFitWidth) + self.rotateLeftAct = QtWidgets.QAction("Rotate &Left", self) + self.rotateLeftAct.setShortcut("l") + self.rotateLeftAct.triggered.connect(self.rotateLeft) + self.rotateRightAct = QtWidgets.QAction("Rotate &Right", self) + self.rotateRightAct.setShortcut("r") + self.rotateRightAct.triggered.connect(self.rotateRight) + self.fullScreenAct = QtWidgets.QAction("Show &Full Screen", self) + self.fullScreenAct.setShortcut("f") + self.fullScreenAct.setCheckable(True) + self.fullScreenAct.triggered.connect(self.fullScreen) + self.imageInfoAct = QtWidgets.QAction("Image &Info", self) + self.imageInfoAct.setShortcut("i") + self.imageInfoAct.triggered.connect(self.imageInfo) + self.overviewAct = QtWidgets.QAction("&Overview Window", self) + self.overviewAct.setShortcut("o") + self.overviewAct.triggered.connect(self.overview) + self.prevImageAct = QtWidgets.QAction("&Previous Image", self) + self.prevImageAct.setShortcut("p") + self.prevImageAct.setEnabled(False) + self.prevImageAct.triggered.connect(self.prevImage) + self.nextImageAct = QtWidgets.QAction("&Next Image", self) + self.nextImageAct.setShortcut("n") + self.nextImageAct.setEnabled(self._haveNext()) + self.nextImageAct.triggered.connect(self.nextImage) + self.selectImageAct = QtWidgets.QAction("&Select Image", self) + self.selectImageAct.setShortcut("s") + self.selectImageAct.setEnabled(not self.readOnly) + self.selectImageAct.triggered.connect(self.selectImage) + self.deselectImageAct = QtWidgets.QAction("&Deselect Image", self) + self.deselectImageAct.setShortcut("d") + self.deselectImageAct.setEnabled(not self.readOnly) + self.deselectImageAct.triggered.connect(self.deselectImage) + self.pushForwardAct = QtWidgets.QAction("Push Image &Forward", self) + self.pushForwardAct.setShortcut("Ctrl+f") + self.pushForwardAct.setEnabled(not self.readOnly) + self.pushForwardAct.triggered.connect(self.pushImageForward) + self.pushBackwardAct = QtWidgets.QAction("Push Image &Backward", self) + self.pushBackwardAct.setShortcut("Ctrl+b") + self.pushBackwardAct.setEnabled(not self.readOnly) + self.pushBackwardAct.triggered.connect(self.pushImageBackward) + self.tagSelectAct = QtWidgets.QAction("&Tags", self) + self.tagSelectAct.setShortcut("t") + self.tagSelectAct.setEnabled(not self.readOnly) + self.tagSelectAct.triggered.connect(self.tagSelect) + + menu = self.menuBar() + + fileMenu = menu.addMenu("&File") + fileMenu.addAction(self.saveAct) + fileMenu.addAction(self.closeAct) + fileMenu.addAction(self.filterOptsAct) + + viewMenu = menu.addMenu("&View") + viewMenu.addAction(self.zoomInAct) + viewMenu.addAction(self.zoomOutAct) + viewMenu.addAction(self.zoomFitHeightAct) + viewMenu.addAction(self.zoomFitWidthAct) + viewMenu.addAction(self.rotateLeftAct) + viewMenu.addAction(self.rotateRightAct) + viewMenu.addSeparator() + viewMenu.addAction(self.fullScreenAct) + viewMenu.addSeparator() + viewMenu.addAction(self.imageInfoAct) + viewMenu.addAction(self.overviewAct) + + imageMenu = menu.addMenu("&Image") + imageMenu.addAction(self.prevImageAct) + imageMenu.addAction(self.nextImageAct) + imageMenu.addAction(self.selectImageAct) + imageMenu.addAction(self.deselectImageAct) + imageMenu.addSeparator() + imageMenu.addAction(self.pushForwardAct) + imageMenu.addAction(self.pushBackwardAct) + imageMenu.addSeparator() + imageMenu.addAction(self.tagSelectAct) self.show() self._extraSize = self.size() - self.scrollArea.viewport().size() @@ -142,34 +160,34 @@ def saveIndex(self): try: self.images.write() self.dirty = False - except photo.index.AlreadyLockedError: - msgBox = QtGui.QMessageBox() + except photoidx.index.AlreadyLockedError: + msgBox = QtWidgets.QMessageBox() msgBox.setWindowTitle("Index is locked") msgBox.setText("Saving the image index failed!") msgBox.setInformativeText("Another process is currently " "accessing the file") - msgBox.setIcon(QtGui.QMessageBox.Critical) + msgBox.setIcon(QtWidgets.QMessageBox.Critical) msgBox.exec_() def close(self): if self.dirty: - msgBox = QtGui.QMessageBox() + msgBox = QtWidgets.QMessageBox() msgBox.setWindowTitle("Save index?") msgBox.setText("The image index been modified.") msgBox.setInformativeText("Save changes before closing?") - msgBox.setIcon(QtGui.QMessageBox.Question) - msgBox.setStandardButtons(QtGui.QMessageBox.Save | - QtGui.QMessageBox.Discard | - QtGui.QMessageBox.Cancel) - msgBox.setDefaultButton(QtGui.QMessageBox.Save) + msgBox.setIcon(QtWidgets.QMessageBox.Question) + msgBox.setStandardButtons(QtWidgets.QMessageBox.Save | + QtWidgets.QMessageBox.Discard | + QtWidgets.QMessageBox.Cancel) + msgBox.setDefaultButton(QtWidgets.QMessageBox.Save) ret = msgBox.exec_() - if ret == QtGui.QMessageBox.Save: + if ret == QtWidgets.QMessageBox.Save: self.saveIndex() if self.dirty: return - elif ret == QtGui.QMessageBox.Discard: + elif ret == QtWidgets.QMessageBox.Discard: pass - elif ret == QtGui.QMessageBox.Cancel: + elif ret == QtWidgets.QMessageBox.Cancel: return if self.overviewwindow: self.overviewwindow.close() @@ -284,6 +302,23 @@ def _reevalFilter(self): self._loadImage() self._checkActions() + def get_visible_center(self): + """Get the center of the current visible area in image coordinates. + """ + hscroll_pos = self.scrollArea.horizontalScrollBar().value() + vscroll_pos = self.scrollArea.verticalScrollBar().value() + scroll_pos = QtCore.QSize(hscroll_pos, vscroll_pos) + win_center = scroll_pos + self.scrollArea.viewport().size() / 2 + return win_center / self.scaleFactor + + def scroll_visible_center_to(self, pos): + """Move the scrollbars to center the position in the visible area. + """ + win_center = self.scaleFactor * pos + scroll_pos = win_center - self.scrollArea.viewport().size() / 2 + self.scrollArea.horizontalScrollBar().setValue(scroll_pos.width()) + self.scrollArea.verticalScrollBar().setValue(scroll_pos.height()) + def zoomIn(self): self.scaleImage(1.6) @@ -417,8 +452,10 @@ def filterOptions(self): self.moveCurrentTo(cur) def scaleImage(self, factor): + center = self.get_visible_center() self.scaleFactor *= factor self._setSize() + self.scroll_visible_center_to(center) def pushImageForward(self): """Move the current image forward one position in the image order. diff --git a/photo/qt/overviewWindow.py b/photoidx/qt/overviewWindow.py similarity index 90% rename from photo/qt/overviewWindow.py rename to photoidx/qt/overviewWindow.py index 90cec20..6c96c13 100644 --- a/photo/qt/overviewWindow.py +++ b/photoidx/qt/overviewWindow.py @@ -3,10 +3,10 @@ import sys import math -from PySide import QtCore, QtGui +from PySide2 import QtCore, QtWidgets -class ThumbnailWidget(QtGui.QLabel): +class ThumbnailWidget(QtWidgets.QLabel): def __init__(self, image): super().__init__() @@ -45,7 +45,7 @@ def markActive(self, isActive): self.setPalette(palette) -class OverviewWindow(QtGui.QMainWindow): +class OverviewWindow(QtWidgets.QMainWindow): def __init__(self, imageViewer): super().__init__() @@ -54,22 +54,23 @@ def __init__(self, imageViewer): self.numcolumns = 4 self.setWindowTitle("Overview") - self.mainLayout = QtGui.QGridLayout() + self.mainLayout = QtWidgets.QGridLayout() self._populate() - centralWidget = QtGui.QWidget() + centralWidget = QtWidgets.QWidget() centralWidget.setLayout(self.mainLayout) - scrollArea = QtGui.QScrollArea() + scrollArea = QtWidgets.QScrollArea() scrollArea.setWidget(centralWidget) scrollArea.setAlignment(QtCore.Qt.AlignCenter) self.setCentralWidget(scrollArea) - self.closeAct = QtGui.QAction("&Close", self, - triggered=self.close) + self.closeAct = QtWidgets.QAction("&Close", self) + self.closeAct.triggered.connect(self.close) - self.fileMenu = QtGui.QMenu("&File", self) + menu = self.menuBar() + + self.fileMenu = menu.addMenu("&File") self.fileMenu.addAction(self.closeAct) - self.menuBar().addMenu(self.fileMenu) # Set the width of the window such that the scrollArea just # fits. We need to add 24 to the central widget, 20 for the diff --git a/photo/qt/tagSelectDialog.py b/photoidx/qt/tagSelectDialog.py similarity index 81% rename from photo/qt/tagSelectDialog.py rename to photoidx/qt/tagSelectDialog.py index 692fbeb..42a9270 100644 --- a/photo/qt/tagSelectDialog.py +++ b/photoidx/qt/tagSelectDialog.py @@ -2,31 +2,31 @@ """ import math -from PySide import QtCore, QtGui +from PySide2 import QtCore, QtWidgets -class TagSelectDialog(QtGui.QDialog): +class TagSelectDialog(QtWidgets.QDialog): def __init__(self, taglist): super().__init__() - self.checkLayout = QtGui.QGridLayout() + self.checkLayout = QtWidgets.QGridLayout() self.settags(taglist) - self.entry = QtGui.QLineEdit() + self.entry = QtWidgets.QLineEdit() self.entry.returnPressed.connect(self.newtag) - entryLabel = QtGui.QLabel("New tag:") + entryLabel = QtWidgets.QLabel("New tag:") entryLabel.setBuddy(self.entry) - entryLayout = QtGui.QHBoxLayout() + entryLayout = QtWidgets.QHBoxLayout() entryLayout.addWidget(entryLabel) entryLayout.addWidget(self.entry) - buttonBox = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Ok | - QtGui.QDialogButtonBox.Cancel) + btn = QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel + buttonBox = QtWidgets.QDialogButtonBox(btn) buttonBox.accepted.connect(self.accept) buttonBox.rejected.connect(self.reject) - mainLayout = QtGui.QVBoxLayout() + mainLayout = QtWidgets.QVBoxLayout() mainLayout.addLayout(self.checkLayout) mainLayout.addLayout(entryLayout) mainLayout.addWidget(buttonBox, alignment=QtCore.Qt.AlignHCenter) @@ -55,7 +55,7 @@ def settags(self, tags): c = 0 for t in sorted(self.taglist): if t not in self.tagCheck: - self.tagCheck[t] = QtGui.QCheckBox(t) + self.tagCheck[t] = QtWidgets.QCheckBox(t) cb = self.tagCheck[t] self.checkLayout.addWidget(cb, c % nrow, c // nrow) c += 1 diff --git a/photo/stats.py b/photoidx/stats.py similarity index 100% rename from photo/stats.py rename to photoidx/stats.py diff --git a/python-photoidx.spec b/python-photoidx.spec new file mode 100644 index 0000000..90ffe5c --- /dev/null +++ b/python-photoidx.spec @@ -0,0 +1,89 @@ +%bcond_without tests +%global distname photoidx + +Name: python3-%{distname} +Version: $version +Release: 0 +Url: $url +Summary: $description +License: Apache-2.0 +Group: Productivity/Graphics/Viewers +Source: %{distname}-%{version}.tar.gz +BuildRequires: fdupes +BuildRequires: python3-base >= 3.6 +BuildRequires: python3-setuptools +%if %{with tests} +BuildRequires: python3-pytest +BuildRequires: python3-distutils-pytest +BuildRequires: python3-pytest-dependency +BuildRequires: python3-PyYAML +BuildRequires: python3-ExifRead >= 2.2.0 +%endif +Provides: python3-photo = %{version}-%{release} +Obsoletes: python3-photo < %{version}-%{release} +Requires: python3-PyYAML +Requires: python3-ExifRead >= 2.2.0 +BuildArch: noarch +BuildRoot: %{_tmppath}/%{name}-%{version}-build + +%description +This package maintains indices for photo collections. The index is +stored as a YAML file and contains metadata and tags describing the +photos. The photos are accessed read only. + +This package provides a Python library and a command line tool for +creating and managing the index. + + +%package qt +Summary: Tools for managing photo collections +Provides: python3-photo-qt = %{version}-%{release} +Obsoletes: python3-photo-qt < %{version}-%{release} +Requires: python3-%{distname} = %{version} +Requires: python3-pyside2 +Recommends: python3-vignette >= 4.3.0 + +%description qt +This package maintains indices for photo collections. The index is +stored as a YAML file and contains metadata and tags describing the +photos. The photos are accessed read only. + +This package provides an image viewer. + + +%prep +%setup -q -n %{distname}-%{version} + + +%build +python3 setup.py build + + +%install +python3 setup.py install --optimize=1 --prefix=%{_prefix} --root=%{buildroot} +%__mv %{buildroot}%{_bindir}/photo-idx.py %{buildroot}%{_bindir}/photo-idx +%__mv %{buildroot}%{_bindir}/imageview.py %{buildroot}%{_bindir}/imageview +%fdupes %{buildroot} + + +%if %{with tests} +%check +python3 setup.py test +%endif + + +%files +%defattr(-,root,root) +%doc README.rst CHANGES.rst +%license LICENSE.txt +%{python3_sitelib}/* +%exclude %{python3_sitelib}/photoidx/qt +%{_bindir}/photo-idx + +%files qt +%defattr(-,root,root) +%{python3_sitelib}/photoidx/qt +%{_bindir}/imageview + + +%changelog diff --git a/python3-photo.spec b/python3-photo.spec deleted file mode 100644 index 4184860..0000000 --- a/python3-photo.spec +++ /dev/null @@ -1,80 +0,0 @@ -%define pkgname photo - -Name: python3-%{pkgname} -Version: 0.9.3 -Release: 1 -Summary: Tools for managing photo collections -License: Apache-2.0 -Group: Development/Languages/Python -Url: https://github.com/RKrahl/photo-tools -Source: %{pkgname}-%{version}.tar.gz -BuildArch: noarch -BuildRequires: python3-devel -BuildRequires: python3-PyYAML -BuildRequires: python3-gexiv2 -BuildRequires: python3-pytest -%if 0%{?sle_version} >= 150000 || 0%{?sle_version} == 120300 -BuildRequires: python3-pytest-dependency -%endif -BuildRequires: python3-distutils-pytest -Requires: python3-PyYAML -Requires: python3-gexiv2 -%if 0%{?suse_version} -BuildRequires: fdupes -%endif -BuildRoot: %{_tmppath}/%{name}-%{version}-build - -%description -This package provides a Python library and a command line tool for -maintaining tags in a collection of photos. - - -%package qt -Summary: Tools for managing photo collections -Requires: python3-%{pkgname} = %{version} -Requires: python3-pyside -Recommends: python3-vignette >= 4.3.0 - -%description qt -This package provides an image viewer for collection of photos. - - -%prep -%setup -q -n %{pkgname}-%{version} - - -%build -python3 setup.py build - - -%install -python3 setup.py install --optimize=1 --prefix=%{_prefix} --root=%{buildroot} -%__mv %{buildroot}%{_bindir}/photoidx.py %{buildroot}%{_bindir}/photoidx -%__mv %{buildroot}%{_bindir}/imageview.py %{buildroot}%{_bindir}/imageview -%if 0%{?suse_version} -%fdupes %{buildroot} -%endif - - -%check -python3 setup.py test - - -%clean -rm -rf %{buildroot} - - -%files -%defattr(-,root,root) -%doc README.rst CHANGES -%{python3_sitelib}/* -%exclude %{python3_sitelib}/photo/qt -%{_bindir}/photoidx - -%files qt -%defattr(-,root,root) -%{python3_sitelib}/photo/qt -%{_bindir}/imageview - - -%changelog diff --git a/imageview.py b/scripts/imageview.py similarity index 70% rename from imageview.py rename to scripts/imageview.py index 797bdc0..2d06eeb 100755 --- a/imageview.py +++ b/scripts/imageview.py @@ -2,10 +2,10 @@ import sys import argparse -from PySide import QtGui -import photo.index -import photo.idxfilter -from photo.qt import ImageViewer +from PySide2 import QtWidgets +import photoidx.index +import photoidx.idxfilter +from photoidx.qt import ImageViewer argparser = argparse.ArgumentParser() @@ -17,22 +17,22 @@ argparser.add_argument('--create', help='create the index if not present', action='store_const', const=True) -photo.idxfilter.addFilterArguments(argparser) +photoidx.idxfilter.addFilterArguments(argparser) args = argparser.parse_args() -app = QtGui.QApplication([]) +app = QtWidgets.QApplication([]) try: - idx = photo.index.Index(idxfile=args.directory) + idx = photoidx.index.Index(idxfile=args.directory) readOnly = args.readOnly dirty = False except OSError: - idx = photo.index.Index(imgdir=args.directory) + idx = photoidx.index.Index(imgdir=args.directory) if args.readOnly: readOnly = True dirty = False else: readOnly = not args.create dirty = args.create -idxfilter = photo.idxfilter.IdxFilter.from_args(args) +idxfilter = photoidx.idxfilter.IdxFilter.from_args(args) imageViewer = ImageViewer(idx, idxfilter, args.scale, readOnly, dirty) sys.exit(app.exec_()) diff --git a/photoidx.py b/scripts/photo-idx.py similarity index 67% rename from photoidx.py rename to scripts/photo-idx.py index 271794d..38c37f9 100755 --- a/photoidx.py +++ b/scripts/photo-idx.py @@ -1,21 +1,21 @@ #! /usr/bin/python import argparse -import photo.index -import photo.idxfilter -from photo.stats import Stats +import photoidx.index +import photoidx.idxfilter +from photoidx.stats import Stats def create(args): idxfile = args.directory if args.update else None hashalg = args.checksums.split(',') if args.checksums else [] - with photo.index.Index(idxfile=idxfile, imgdir=args.directory, - hashalg=hashalg) as idx: + with photoidx.index.Index(idxfile=idxfile, imgdir=args.directory, + hashalg=hashalg) as idx: idx.write() def ls(args): - with photo.index.Index(idxfile=args.directory) as idx: - idxfilter = photo.idxfilter.IdxFilter.from_args(args) + with photoidx.index.Index(idxfile=args.directory) as idx: + idxfilter = photoidx.idxfilter.IdxFilter.from_args(args) for i in idxfilter.filter(idx): if args.checksum: try: @@ -27,8 +27,8 @@ def ls(args): print(i.filename) def lstags(args): - with photo.index.Index(idxfile=args.directory) as idx: - idxfilter = photo.idxfilter.IdxFilter.from_args(args) + with photoidx.index.Index(idxfile=args.directory) as idx: + idxfilter = photoidx.idxfilter.IdxFilter.from_args(args) tags = set() for i in idxfilter.filter(idx): tags.update(i.tags) @@ -36,36 +36,36 @@ def lstags(args): print(t) def addtag(args): - with photo.index.Index(idxfile=args.directory) as idx: - idxfilter = photo.idxfilter.IdxFilter.from_args(args) + with photoidx.index.Index(idxfile=args.directory) as idx: + idxfilter = photoidx.idxfilter.IdxFilter.from_args(args) for i in idxfilter.filter(idx): i.tags.add(args.tag) idx.write() def rmtag(args): - with photo.index.Index(idxfile=args.directory) as idx: - idxfilter = photo.idxfilter.IdxFilter.from_args(args) + with photoidx.index.Index(idxfile=args.directory) as idx: + idxfilter = photoidx.idxfilter.IdxFilter.from_args(args) for i in idxfilter.filter(idx): i.tags.discard(args.tag) idx.write() def select(args): - with photo.index.Index(idxfile=args.directory) as idx: - idxfilter = photo.idxfilter.IdxFilter.from_args(args) + with photoidx.index.Index(idxfile=args.directory) as idx: + idxfilter = photoidx.idxfilter.IdxFilter.from_args(args) for i in idxfilter.filter(idx): i.selected = True idx.write() def deselect(args): - with photo.index.Index(idxfile=args.directory) as idx: - idxfilter = photo.idxfilter.IdxFilter.from_args(args) + with photoidx.index.Index(idxfile=args.directory) as idx: + idxfilter = photoidx.idxfilter.IdxFilter.from_args(args) for i in idxfilter.filter(idx): i.selected = False idx.write() def stats(args): - with photo.index.Index(idxfile=args.directory) as idx: - idxfilter = photo.idxfilter.IdxFilter.from_args(args) + with photoidx.index.Index(idxfile=args.directory) as idx: + idxfilter = photoidx.idxfilter.IdxFilter.from_args(args) stats = Stats(idxfilter.filter(idx)) print(str(stats)) @@ -85,35 +85,35 @@ def stats(args): ls_parser = subparsers.add_parser('ls', help="list image files") ls_parser.add_argument('--checksum', help="hash algorithm to print checksums") -photo.idxfilter.addFilterArguments(ls_parser) +photoidx.idxfilter.addFilterArguments(ls_parser) ls_parser.set_defaults(func=ls) lstags_parser = subparsers.add_parser('lstags', help="list tags") -photo.idxfilter.addFilterArguments(lstags_parser) +photoidx.idxfilter.addFilterArguments(lstags_parser) lstags_parser.set_defaults(func=lstags) addtag_parser = subparsers.add_parser('addtag', help="add tag to images") addtag_parser.add_argument('tag') -photo.idxfilter.addFilterArguments(addtag_parser) +photoidx.idxfilter.addFilterArguments(addtag_parser) addtag_parser.set_defaults(func=addtag) rmtag_parser = subparsers.add_parser('rmtag', help="remove tag from images") rmtag_parser.add_argument('tag') -photo.idxfilter.addFilterArguments(rmtag_parser) +photoidx.idxfilter.addFilterArguments(rmtag_parser) rmtag_parser.set_defaults(func=rmtag) select_parser = subparsers.add_parser('select', help="add images to the selection") -photo.idxfilter.addFilterArguments(select_parser) +photoidx.idxfilter.addFilterArguments(select_parser) select_parser.set_defaults(func=select) deselect_parser = subparsers.add_parser('deselect', help="remove images from the selection") -photo.idxfilter.addFilterArguments(deselect_parser) +photoidx.idxfilter.addFilterArguments(deselect_parser) deselect_parser.set_defaults(func=deselect) stats_parser = subparsers.add_parser('stats', help="show statistics") -photo.idxfilter.addFilterArguments(stats_parser) +photoidx.idxfilter.addFilterArguments(stats_parser) stats_parser.set_defaults(func=stats) args = argparser.parse_args() diff --git a/setup.py b/setup.py index ac93421..4f0c4e5 100644 --- a/setup.py +++ b/setup.py @@ -1,42 +1,130 @@ -#! /usr/bin/python +"""Maintain indices for photo collections -from distutils.core import setup +This package maintains indices for photo collections. The index is +stored as a YAML file and contains metadata and tags describing the +photos. The photos are accessed read only. +""" + +import setuptools +from setuptools import setup +import setuptools.command.build_py +import distutils.command.sdist +from distutils import log +from glob import glob +from pathlib import Path +import string try: import distutils_pytest -except ImportError: - pass -import photo -import re + cmdclass = distutils_pytest.cmdclass +except (ImportError, AttributeError): + cmdclass = dict() +try: + import setuptools_scm + version = setuptools_scm.get_version() +except (ImportError, LookupError): + try: + import _meta + version = _meta.__version__ + except ImportError: + log.warn("warning: cannot determine version number") + version = "UNKNOWN" + +docstring = __doc__ + + +class meta(setuptools.Command): + + description = "generate meta files" + user_options = [] + init_template = '''"""%(doc)s""" + +__version__ = "%(version)s" +''' + meta_template = ''' +__version__ = "%(version)s" +''' + + def initialize_options(self): + self.package_dir = None + + def finalize_options(self): + self.package_dir = {} + if self.distribution.package_dir: + for name, path in self.distribution.package_dir.items(): + self.package_dir[name] = convert_path(path) + + def run(self): + values = { + 'version': self.distribution.get_version(), + 'doc': docstring + } + try: + pkgname = self.distribution.packages[0] + except IndexError: + log.warn("warning: no package defined") + else: + pkgdir = Path(self.package_dir.get(pkgname, pkgname)) + if not pkgdir.is_dir(): + pkgdir.mkdir() + with (pkgdir / "__init__.py").open("wt") as f: + print(self.init_template % values, file=f) + with Path("_meta.py").open("wt") as f: + print(self.meta_template % values, file=f) + + +# Note: Do not use setuptools for making the source distribution, +# rather use the good old distutils instead. +# Rationale: https://rhodesmill.org/brandon/2009/eby-magic/ +class sdist(distutils.command.sdist.sdist): + def run(self): + self.run_command('meta') + super().run() + subst = { + "version": self.distribution.get_version(), + "url": self.distribution.get_url(), + "description": docstring.split("\n")[0], + "long_description": docstring.split("\n", maxsplit=2)[2].strip(), + } + for spec in glob("*.spec"): + with Path(spec).open('rt') as inf: + with Path(self.dist_dir, spec).open('wt') as outf: + outf.write(string.Template(inf.read()).substitute(subst)) + + +class build_py(setuptools.command.build_py.build_py): + def run(self): + self.run_command('meta') + super().run() -DOCLINES = photo.__doc__.split("\n") -DESCRIPTION = DOCLINES[0] -LONG_DESCRIPTION = "\n".join(DOCLINES[2:]) -VERSION = photo.__version__ -AUTHOR = photo.__author__ -m = re.match(r"^(.*?)\s*<(.*)>$", AUTHOR) -(AUTHOR_NAME, AUTHOR_EMAIL) = m.groups() if m else (AUTHOR, None) +with Path("README.rst").open("rt", encoding="utf8") as f: + readme = f.read() setup( - name = "photo", - version = VERSION, - description = DESCRIPTION, - long_description = LONG_DESCRIPTION, - author = AUTHOR_NAME, - author_email = AUTHOR_EMAIL, + name = "photoidx", + version = version, + description = docstring.split("\n")[0], + long_description = readme, + url = "https://github.com/RKrahl/photoidx", + author = "Rolf Krahl", + author_email = "rolf@rotkraut.de", license = "Apache-2.0", - requires = ["PyYAML"], - packages = ["photo", "photo.qt"], - scripts = ["photoidx.py", "imageview.py"], classifiers = [ "Development Status :: 5 - Production/Stable", + "Intended Audience :: End Users/Desktop", "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Programming Language :: Python", - "Programming Language :: Python :: 3.4", - "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", - ], + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + ], + packages = ["photoidx", "photoidx.qt"], + scripts = ["scripts/photo-idx.py", "scripts/imageview.py"], + python_requires = ">=3.6", + install_requires = ["PyYAML", "ExifRead >= 2.2.0"], + cmdclass = dict(cmdclass, build_py=build_py, sdist=sdist, meta=meta), ) diff --git a/tests/conftest.py b/tests/conftest.py index 5358448..f9d26b3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,6 +8,7 @@ import sys import tempfile import pytest +import photoidx testdir = Path(__file__).parent @@ -17,23 +18,11 @@ def gettestdata(fname): assert fname.is_file() return str(fname) -class TmpDir(object): - """Provide a temporary directory. - """ - def __init__(self): - self.dir = Path(tempfile.mkdtemp(prefix="photo-test-")) - def __del__(self): - self.cleanup() - def cleanup(self): - if self.dir: - shutil.rmtree(str(self.dir)) - self.dir = None - @pytest.fixture(scope="module") def tmpdir(request): - td = TmpDir() - request.addfinalizer(td.cleanup) - return td.dir + td = tempfile.mkdtemp(prefix="photoidx-test-") + yield Path(td) + shutil.rmtree(td) def callscript(scriptname, args, stdin=None, stdout=None, stderr=None): try: @@ -44,3 +33,10 @@ def callscript(scriptname, args, stdin=None, stdout=None, stderr=None): cmd = [sys.executable, str(script)] + args print("\n>", *cmd) subprocess.check_call(cmd, stdin=stdin, stdout=stdout, stderr=stderr) + +def pytest_report_header(config): + """Add information on the package version used in the tests. + """ + modpath = Path(photoidx.__file__).resolve().parent + return [ "photoidx: %s" % (photoidx.__version__), + " %s" % (modpath) ] diff --git a/tests/data/index-create.yaml b/tests/data/index-create.yaml index a7b5787..a29a62a 100644 --- a/tests/data/index-create.yaml +++ b/tests/data/index-create.yaml @@ -4,7 +4,7 @@ filename: dsc_4623.jpg gpsPosition: E: 139.76603333333333 - N: 35.671189999999996 + N: 35.67119 orientation: Rotate 90 CW tags: [] - checksum: @@ -13,7 +13,7 @@ filename: dsc_4664.jpg gpsPosition: E: 139.70024333333333 - N: 35.675639999999994 + N: 35.67564 orientation: Horizontal (normal) tags: [] - checksum: @@ -21,7 +21,7 @@ createDate: 2016-03-05 14:53:42 filename: dsc_4831.jpg gpsPosition: - E: 139.02553666666668 + E: 139.02553666666665 N: 35.20459666666667 orientation: Horizontal (normal) tags: [] diff --git a/tests/data/index-legacy.yaml b/tests/data/index-legacy.yaml index 1bf55a8..f0b842c 100644 --- a/tests/data/index-legacy.yaml +++ b/tests/data/index-legacy.yaml @@ -2,7 +2,7 @@ filename: dsc_4623.jpg gpsPosition: E: 139.76603333333333 - N: 35.671189999999996 + N: 35.67119 md5: e08205e982f5649088ab0ef047d60706 orientation: Rotate 90 CW tags: [] @@ -10,14 +10,14 @@ filename: dsc_4664.jpg gpsPosition: E: 139.70024333333333 - N: 35.675639999999994 + N: 35.67564 md5: bc8605c7394fbf818dbc694776db6534 orientation: Horizontal (normal) tags: [] - createdate: 2016-03-05 14:53:42 filename: dsc_4831.jpg gpsPosition: - E: 139.02553666666668 + E: 139.02553666666665 N: 35.20459666666667 md5: 8753a712e0f426a7ac8b9c47368b7528 orientation: Horizontal (normal) diff --git a/tests/data/index-subdirs.yaml b/tests/data/index-subdirs.yaml index 59ffd80..9c737c9 100644 --- a/tests/data/index-subdirs.yaml +++ b/tests/data/index-subdirs.yaml @@ -4,7 +4,7 @@ filename: Japan/dsc_4623.jpg gpsPosition: E: 139.76603333333333 - N: 35.671189999999996 + N: 35.67119 orientation: Rotate 90 CW tags: [] - checksum: @@ -13,7 +13,7 @@ filename: Japan/dsc_4664.jpg gpsPosition: E: 139.70024333333333 - N: 35.675639999999994 + N: 35.67564 orientation: Horizontal (normal) tags: [] - checksum: @@ -21,7 +21,7 @@ createDate: 2016-03-05 14:53:42 filename: Japan/dsc_4831.jpg gpsPosition: - E: 139.02553666666668 + E: 139.02553666666665 N: 35.20459666666667 orientation: Horizontal (normal) tags: [] @@ -48,7 +48,7 @@ createDate: 2017-09-28 11:41:25 filename: Quebec/dsc_7490.jpg gpsPosition: - N: 46.81007333333333 + N: 46.810073333333335 W: 71.20413333333333 orientation: Horizontal (normal) tags: [] diff --git a/tests/data/index-unicode-tags.yaml b/tests/data/index-unicode-tags.yaml index deb47b0..f9f9b63 100644 --- a/tests/data/index-unicode-tags.yaml +++ b/tests/data/index-unicode-tags.yaml @@ -4,7 +4,7 @@ filename: dsc_4623.jpg gpsPosition: E: 139.76603333333333 - N: 35.671189999999996 + N: 35.67119 orientation: Rotate 90 CW tags: - Ginza @@ -15,7 +15,7 @@ filename: dsc_4664.jpg gpsPosition: E: 139.70024333333333 - N: 35.675639999999994 + N: 35.67564 orientation: Horizontal (normal) tags: - "Meiji-jing\u016B" @@ -26,7 +26,7 @@ createDate: 2016-03-05 14:53:42 filename: dsc_4831.jpg gpsPosition: - E: 139.02553666666668 + E: 139.02553666666665 N: 35.20459666666667 orientation: Horizontal (normal) tags: diff --git a/tests/pytest.ini b/tests/pytest.ini new file mode 100644 index 0000000..bc0a16c --- /dev/null +++ b/tests/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +minversion = 3.0.0 diff --git a/tests/test_01_create_index.py b/tests/test_01_create_index.py index 5b24c72..2f53714 100644 --- a/tests/test_01_create_index.py +++ b/tests/test_01_create_index.py @@ -4,7 +4,7 @@ import filecmp import shutil import pytest -import photo.index +import photoidx.index from conftest import tmpdir, gettestdata testimgs = [ @@ -25,7 +25,7 @@ def test_create_curdir(imgdir, monkeypatch): """Create a new index in the current directory adding all images. """ monkeypatch.chdir(str(imgdir)) - with photo.index.Index(imgdir=".") as idx: + with photoidx.index.Index(imgdir=".") as idx: idx.write() idxfile = ".index.yaml" assert filecmp.cmp(refindex, idxfile), "index file differs from reference" @@ -34,7 +34,7 @@ def test_create_curdir(imgdir, monkeypatch): def test_create(imgdir): """Create a new index adding all images in the imgdir. """ - with photo.index.Index(imgdir=imgdir) as idx: + with photoidx.index.Index(imgdir=imgdir) as idx: idx.write() idxfile = str(imgdir / ".index.yaml") assert filecmp.cmp(refindex, idxfile), "index file differs from reference" @@ -43,7 +43,7 @@ def test_create(imgdir): def test_read(imgdir): """Read the index file and write it out again. """ - with photo.index.Index(idxfile=imgdir) as idx: + with photoidx.index.Index(idxfile=imgdir) as idx: idx.write() idxfile = str(imgdir / ".index.yaml") assert filecmp.cmp(refindex, idxfile), "index file differs from reference" diff --git a/tests/test_01_filter.py b/tests/test_01_filter.py index 9d67895..c19a5a0 100644 --- a/tests/test_01_filter.py +++ b/tests/test_01_filter.py @@ -5,9 +5,9 @@ import filecmp import shutil import pytest -import photo.index -import photo.idxfilter -from photo.geo import GeoPosition +import photoidx.index +import photoidx.idxfilter +from photoidx.geo import GeoPosition from conftest import tmpdir, gettestdata testimgs = [ @@ -26,35 +26,35 @@ def imgdir(tmpdir): def test_by_date(imgdir): """Select by date. """ - with photo.index.Index(idxfile=imgdir) as idx: + with photoidx.index.Index(idxfile=imgdir) as idx: date = (datetime.datetime(2016, 3, 5), datetime.datetime(2016, 3, 6)) - idxfilter = photo.idxfilter.IdxFilter(date=date) + idxfilter = photoidx.idxfilter.IdxFilter(date=date) fnames = [ str(i.filename) for i in idxfilter.filter(idx) ] assert fnames == ["dsc_4831.jpg"] def test_by_gpspos(imgdir): """Select by GPS position. """ - with photo.index.Index(idxfile=imgdir) as idx: + with photoidx.index.Index(idxfile=imgdir) as idx: pos = GeoPosition("35.6883 N, 139.7544 E") - idxfilter = photo.idxfilter.IdxFilter(gpspos=pos, gpsradius=20.0) + idxfilter = photoidx.idxfilter.IdxFilter(gpspos=pos, gpsradius=20.0) fnames = [ str(i.filename) for i in idxfilter.filter(idx) ] assert fnames == ["dsc_4623.jpg", "dsc_4664.jpg"] def test_by_files(imgdir): """Select by file names. """ - with photo.index.Index(idxfile=imgdir) as idx: + with photoidx.index.Index(idxfile=imgdir) as idx: files = ["dsc_4664.jpg", "dsc_4831.jpg"] - idxfilter = photo.idxfilter.IdxFilter(files=files) + idxfilter = photoidx.idxfilter.IdxFilter(files=files) fnames = [ str(i.filename) for i in idxfilter.filter(idx) ] assert fnames == files def test_by_single_tag(imgdir): """Select by one single tag. """ - with photo.index.Index(idxfile=imgdir) as idx: - idxfilter = photo.idxfilter.IdxFilter(tags="Shinto_shrine") + with photoidx.index.Index(idxfile=imgdir) as idx: + idxfilter = photoidx.idxfilter.IdxFilter(tags="Shinto_shrine") fnames = [ str(i.filename) for i in idxfilter.filter(idx) ] assert fnames == ["dsc_4664.jpg", "dsc_4831.jpg"] @@ -64,8 +64,8 @@ def test_by_mult_tags(imgdir): Combining multiple tags acts like an and, it selects only images having all the tags set. """ - with photo.index.Index(idxfile=imgdir) as idx: - idxfilter = photo.idxfilter.IdxFilter(tags="Tokyo,Shinto_shrine") + with photoidx.index.Index(idxfile=imgdir) as idx: + idxfilter = photoidx.idxfilter.IdxFilter(tags="Tokyo,Shinto_shrine") fnames = [ str(i.filename) for i in idxfilter.filter(idx) ] assert fnames == ["dsc_4664.jpg"] @@ -75,8 +75,8 @@ def test_by_neg_tags(imgdir): Prepending a tag by an exclamation mark selects the images having the tag not set. """ - with photo.index.Index(idxfile=imgdir) as idx: - idxfilter = photo.idxfilter.IdxFilter(tags="Tokyo,!Shinto_shrine") + with photoidx.index.Index(idxfile=imgdir) as idx: + idxfilter = photoidx.idxfilter.IdxFilter(tags="Tokyo,!Shinto_shrine") fnames = [ str(i.filename) for i in idxfilter.filter(idx) ] assert fnames == ["dsc_4623.jpg"] @@ -85,8 +85,8 @@ def test_by_empty_tag(imgdir): The empty string as tag selects images having no tag. """ - with photo.index.Index(idxfile=imgdir) as idx: - idxfilter = photo.idxfilter.IdxFilter(tags="") + with photoidx.index.Index(idxfile=imgdir) as idx: + idxfilter = photoidx.idxfilter.IdxFilter(tags="") fnames = [ str(i.filename) for i in idxfilter.filter(idx) ] assert fnames == ["dsc_5126.jpg", "dsc_5167.jpg"] @@ -96,33 +96,33 @@ def test_by_date_and_tag(imgdir): Multiple selection criteria, such as date and tags may be combined. """ - with photo.index.Index(idxfile=imgdir) as idx: + with photoidx.index.Index(idxfile=imgdir) as idx: date = (datetime.datetime(2016, 2, 28), datetime.datetime(2016, 2, 29)) - idxfilter = photo.idxfilter.IdxFilter(tags="Tokyo", date=date) + idxfilter = photoidx.idxfilter.IdxFilter(tags="Tokyo", date=date) fnames = [ str(i.filename) for i in idxfilter.filter(idx) ] assert fnames == ["dsc_4623.jpg"] def test_by_selected(imgdir): """Select by selected flag. """ - with photo.index.Index(idxfile=imgdir) as idx: - idxfilter = photo.idxfilter.IdxFilter(select=True) + with photoidx.index.Index(idxfile=imgdir) as idx: + idxfilter = photoidx.idxfilter.IdxFilter(select=True) fnames = [ str(i.filename) for i in idxfilter.filter(idx) ] assert fnames == ["dsc_4664.jpg", "dsc_5126.jpg"] def test_by_selected_and_tag(imgdir): """Select by selected flag and tag. """ - with photo.index.Index(idxfile=imgdir) as idx: - idxfilter = photo.idxfilter.IdxFilter(select=True, tags="Tokyo") + with photoidx.index.Index(idxfile=imgdir) as idx: + idxfilter = photoidx.idxfilter.IdxFilter(select=True, tags="Tokyo") fnames = [ str(i.filename) for i in idxfilter.filter(idx) ] assert fnames == ["dsc_4664.jpg"] def test_by_not_selected(imgdir): """Select by not-selected flag. """ - with photo.index.Index(idxfile=imgdir) as idx: - idxfilter = photo.idxfilter.IdxFilter(select=False) + with photoidx.index.Index(idxfile=imgdir) as idx: + idxfilter = photoidx.idxfilter.IdxFilter(select=False) fnames = [ str(i.filename) for i in idxfilter.filter(idx) ] assert fnames == ["dsc_4623.jpg", "dsc_4831.jpg", "dsc_5167.jpg"] diff --git a/tests/test_01_filter_date.py b/tests/test_01_filter_date.py index 895f62d..1806eee 100644 --- a/tests/test_01_filter_date.py +++ b/tests/test_01_filter_date.py @@ -3,8 +3,8 @@ import datetime import pytest -import photo.index -import photo.idxfilter +import photoidx.index +import photoidx.idxfilter from conftest import gettestdata testimgs = [ "dsc_%04d.jpg" % i for i in range(1,13) ] @@ -14,9 +14,9 @@ def test_single_date(): """Select by single date. """ - with photo.index.Index(idxfile=indexfile) as idx: + with photoidx.index.Index(idxfile=indexfile) as idx: date = (datetime.datetime(2016, 2, 29), datetime.datetime(2016, 3, 1)) - idxfilter = photo.idxfilter.IdxFilter(date=date) + idxfilter = photoidx.idxfilter.IdxFilter(date=date) fnames = [ str(i.filename) for i in idxfilter.filter(idx) ] assert fnames == testimgs[1:4] @@ -24,9 +24,9 @@ def test_single_date(): def test_interval_date_date(): """Select by an interval between two dates. """ - with photo.index.Index(idxfile=indexfile) as idx: + with photoidx.index.Index(idxfile=indexfile) as idx: date = (datetime.datetime(2016, 2, 29), datetime.datetime(2016, 3, 6)) - idxfilter = photo.idxfilter.IdxFilter(date=date) + idxfilter = photoidx.idxfilter.IdxFilter(date=date) fnames = [ str(i.filename) for i in idxfilter.filter(idx) ] assert fnames == testimgs[1:11] @@ -34,10 +34,10 @@ def test_interval_date_date(): def test_interval_date_datetime(): """Select by an interval between start date and end date/time. """ - with photo.index.Index(idxfile=indexfile) as idx: + with photoidx.index.Index(idxfile=indexfile) as idx: date = (datetime.datetime(2016, 2, 29), datetime.datetime(2016, 3, 5, 3, 47, 9)) - idxfilter = photo.idxfilter.IdxFilter(date=date) + idxfilter = photoidx.idxfilter.IdxFilter(date=date) fnames = [ str(i.filename) for i in idxfilter.filter(idx) ] assert fnames == testimgs[1:9] @@ -47,10 +47,10 @@ def test_single_datetime(): Probably not very useful in the praxis, but valid. """ - with photo.index.Index(idxfile=indexfile) as idx: + with photoidx.index.Index(idxfile=indexfile) as idx: date = (datetime.datetime(2016, 3, 3, 11, 21, 40), datetime.datetime(2016, 3, 3, 11, 21, 41)) - idxfilter = photo.idxfilter.IdxFilter(date=date) + idxfilter = photoidx.idxfilter.IdxFilter(date=date) fnames = [ str(i.filename) for i in idxfilter.filter(idx) ] assert fnames == testimgs[6:7] @@ -58,10 +58,10 @@ def test_single_datetime(): def test_interval_datetime_date(): """Select by an interval between start date/time and end date. """ - with photo.index.Index(idxfile=indexfile) as idx: + with photoidx.index.Index(idxfile=indexfile) as idx: date = (datetime.datetime(2016, 3, 3, 11, 21, 40), datetime.datetime(2016, 3, 6)) - idxfilter = photo.idxfilter.IdxFilter(date=date) + idxfilter = photoidx.idxfilter.IdxFilter(date=date) fnames = [ str(i.filename) for i in idxfilter.filter(idx) ] assert fnames == testimgs[6:11] @@ -69,9 +69,9 @@ def test_interval_datetime_date(): def test_interval_datetime_datetime(): """Select by an interval between two date/times. """ - with photo.index.Index(idxfile=indexfile) as idx: + with photoidx.index.Index(idxfile=indexfile) as idx: date = (datetime.datetime(2016, 3, 3, 11, 21, 41), datetime.datetime(2016, 3, 5, 3, 47, 9)) - idxfilter = photo.idxfilter.IdxFilter(date=date) + idxfilter = photoidx.idxfilter.IdxFilter(date=date) fnames = [ str(i.filename) for i in idxfilter.filter(idx) ] assert fnames == testimgs[7:9] diff --git a/tests/test_01_filter_date_args.py b/tests/test_01_filter_date_args.py index c94e1cf..cd552ad 100644 --- a/tests/test_01_filter_date_args.py +++ b/tests/test_01_filter_date_args.py @@ -7,8 +7,8 @@ import argparse import pytest -import photo.index -import photo.idxfilter +import photoidx.index +import photoidx.idxfilter from conftest import gettestdata testimgs = [ "dsc_%04d.jpg" % i for i in range(1,13) ] @@ -17,16 +17,16 @@ @pytest.fixture(scope="module") def argparser(): parser = argparse.ArgumentParser() - photo.idxfilter.addFilterArguments(parser) + photoidx.idxfilter.addFilterArguments(parser) return parser def test_single_date(argparser): """Select by single date. """ - with photo.index.Index(idxfile=indexfile) as idx: + with photoidx.index.Index(idxfile=indexfile) as idx: args = argparser.parse_args(["--date=2016-02-29"]) - idxfilter = photo.idxfilter.IdxFilter.from_args(args) + idxfilter = photoidx.idxfilter.IdxFilter.from_args(args) fnames = [ str(i.filename) for i in idxfilter.filter(idx) ] assert fnames == testimgs[1:4] @@ -34,9 +34,9 @@ def test_single_date(argparser): def test_interval_date_date(argparser): """Select by an interval between two dates. """ - with photo.index.Index(idxfile=indexfile) as idx: + with photoidx.index.Index(idxfile=indexfile) as idx: args = argparser.parse_args(["--date=2016-02-29--2016-03-06"]) - idxfilter = photo.idxfilter.IdxFilter.from_args(args) + idxfilter = photoidx.idxfilter.IdxFilter.from_args(args) fnames = [ str(i.filename) for i in idxfilter.filter(idx) ] assert fnames == testimgs[1:11] @@ -44,9 +44,9 @@ def test_interval_date_date(argparser): def test_interval_date_datetime(argparser): """Select by an interval between start date and end date/time. """ - with photo.index.Index(idxfile=indexfile) as idx: + with photoidx.index.Index(idxfile=indexfile) as idx: args = argparser.parse_args(["--date=2016-02-29/2016-03-05T03:47:09"]) - idxfilter = photo.idxfilter.IdxFilter.from_args(args) + idxfilter = photoidx.idxfilter.IdxFilter.from_args(args) fnames = [ str(i.filename) for i in idxfilter.filter(idx) ] assert fnames == testimgs[1:9] @@ -56,9 +56,9 @@ def test_single_datetime(argparser): Probably not very useful in the praxis, but valid. """ - with photo.index.Index(idxfile=indexfile) as idx: + with photoidx.index.Index(idxfile=indexfile) as idx: args = argparser.parse_args(["--date=2016-03-03T11:21:40"]) - idxfilter = photo.idxfilter.IdxFilter.from_args(args) + idxfilter = photoidx.idxfilter.IdxFilter.from_args(args) fnames = [ str(i.filename) for i in idxfilter.filter(idx) ] assert fnames == testimgs[6:7] @@ -66,9 +66,9 @@ def test_single_datetime(argparser): def test_interval_datetime_date(argparser): """Select by an interval between start date/time and end date. """ - with photo.index.Index(idxfile=indexfile) as idx: + with photoidx.index.Index(idxfile=indexfile) as idx: args = argparser.parse_args(["--date=2016-03-03T11:21:40/2016-03-06"]) - idxfilter = photo.idxfilter.IdxFilter.from_args(args) + idxfilter = photoidx.idxfilter.IdxFilter.from_args(args) fnames = [ str(i.filename) for i in idxfilter.filter(idx) ] assert fnames == testimgs[6:11] @@ -76,9 +76,9 @@ def test_interval_datetime_date(argparser): def test_interval_datetime_datetime(argparser): """Select by an interval between two date/times. """ - with photo.index.Index(idxfile=indexfile) as idx: + with photoidx.index.Index(idxfile=indexfile) as idx: args = argparser.parse_args(["--date=2016-03-03T11:21:41" "--2016-03-05T03:47:09"]) - idxfilter = photo.idxfilter.IdxFilter.from_args(args) + idxfilter = photoidx.idxfilter.IdxFilter.from_args(args) fnames = [ str(i.filename) for i in idxfilter.filter(idx) ] assert fnames == testimgs[7:9] diff --git a/tests/test_01_read_write.py b/tests/test_01_read_write.py index 0ac6ea1..e9f04d0 100644 --- a/tests/test_01_read_write.py +++ b/tests/test_01_read_write.py @@ -4,7 +4,7 @@ import filecmp import shutil import pytest -import photo.index +import photoidx.index from conftest import tmpdir, gettestdata testimgs = [ @@ -26,7 +26,7 @@ def test_read_non_existent(imgdir): """Try to read an index file that does not exist. """ with pytest.raises(OSError): - with photo.index.Index(idxfile=imgdir) as idx: + with photoidx.index.Index(idxfile=imgdir) as idx: pass def test_read_write(imgdir): @@ -34,7 +34,7 @@ def test_read_write(imgdir): """ idxfile = str(imgdir / ".index.yaml") shutil.copy(refindex, idxfile) - with photo.index.Index(idxfile=imgdir) as idx: + with photoidx.index.Index(idxfile=imgdir) as idx: idx.write() assert filecmp.cmp(refindex, idxfile), "index file differs from reference" @@ -43,6 +43,6 @@ def test_read_write_unicode(imgdir): """ idxfile = str(imgdir / ".index.yaml") shutil.copy(refindexu, idxfile) - with photo.index.Index(idxfile=imgdir) as idx: + with photoidx.index.Index(idxfile=imgdir) as idx: idx.write() assert filecmp.cmp(refindexu, idxfile), "index file differs from reference" diff --git a/tests/test_02_photoidx.py b/tests/test_02_photoidx.py index ee031e8..c35b8f7 100644 --- a/tests/test_02_photoidx.py +++ b/tests/test_02_photoidx.py @@ -1,4 +1,4 @@ -"""Call the command line script photoidx.py. +"""Call the command line script photo-idx.py. """ import datetime @@ -25,10 +25,10 @@ def imgdir(tmpdir): return tmpdir -# Note: the default value for the "-d" option to photoidx is the +# Note: the default value for the "-d" option to photo-idx is the # current working directory. So changing to imgdir before calling -# photoidx without the "-d" option should be equivalent to calling -# "photoidx -d imgdir". We more or less try both variants at random +# photo-idx without the "-d" option should be equivalent to calling +# "photo-idx -d imgdir". We more or less try both variants at random # in the tests. @pytest.mark.dependency() @@ -36,7 +36,7 @@ def test_create(imgdir, monkeypatch): """Create the index. """ monkeypatch.chdir(str(imgdir)) - callscript("photoidx.py", ["create"]) + callscript("photo-idx.py", ["create"]) idxfile = str(imgdir / ".index.yaml") assert filecmp.cmp(refindex, idxfile), "index file differs from reference" @@ -46,7 +46,7 @@ def test_ls_all(imgdir): """ fname = imgdir / "out" with fname.open("wt") as f: - callscript("photoidx.py", ["-d", str(imgdir), "ls"], stdout=f) + callscript("photo-idx.py", ["-d", str(imgdir), "ls"], stdout=f) with fname.open("rt") as f: out = f.read().split() assert out == testimgs @@ -61,7 +61,7 @@ def test_ls_md5(imgdir, monkeypatch): monkeypatch.chdir(str(imgdir)) fname = imgdir / "md5" with fname.open("wt") as f: - callscript("photoidx.py", ["ls", "--checksum=md5"], stdout=f) + callscript("photo-idx.py", ["ls", "--checksum=md5"], stdout=f) with fname.open("rt") as f: cmd = [md5sum, "-c"] print(">", *cmd) @@ -72,11 +72,11 @@ def test_addtag_all(imgdir): """Tag all images. """ args = ["-d", str(imgdir), "addtag", "all"] - callscript("photoidx.py", args) + callscript("photo-idx.py", args) fname = imgdir / "out" with fname.open("wt") as f: args = ["-d", str(imgdir), "ls", "--tags", "all"] - callscript("photoidx.py", args, stdout=f) + callscript("photo-idx.py", args, stdout=f) with fname.open("rt") as f: out = f.read().split() assert out == testimgs @@ -86,11 +86,11 @@ def test_addtag_by_date(imgdir): """Select by date. """ args = ["-d", str(imgdir), "addtag", "--date", "2016-03-05", "Hakone"] - callscript("photoidx.py", args) + callscript("photo-idx.py", args) fname = imgdir / "out" with fname.open("wt") as f: args = ["-d", str(imgdir), "ls", "--tags", "Hakone"] - callscript("photoidx.py", args, stdout=f) + callscript("photo-idx.py", args, stdout=f) with fname.open("rt") as f: out = f.read().split() assert out == ["dsc_4831.jpg"] @@ -102,11 +102,11 @@ def test_addtag_by_gpspos(imgdir, monkeypatch): monkeypatch.chdir(str(imgdir)) args = ["addtag", "--gpspos", "35.6883 N, 139.7544 E", "--gpsradius", "20.0", "Tokyo"] - callscript("photoidx.py", args) + callscript("photo-idx.py", args) fname = imgdir / "out" with fname.open("wt") as f: args = ["ls", "--tags", "Tokyo"] - callscript("photoidx.py", args, stdout=f) + callscript("photo-idx.py", args, stdout=f) with fname.open("rt") as f: out = f.read().split() assert out == ["dsc_4623.jpg", "dsc_4664.jpg"] @@ -117,11 +117,11 @@ def test_addtag_by_files(imgdir, monkeypatch): """ monkeypatch.chdir(str(imgdir)) args = ["addtag", "Shinto_shrine", "dsc_4664.jpg", "dsc_4831.jpg"] - callscript("photoidx.py", args) + callscript("photo-idx.py", args) fname = imgdir / "out" with fname.open("wt") as f: args = ["ls", "--tags", "Shinto_shrine"] - callscript("photoidx.py", args, stdout=f) + callscript("photo-idx.py", args, stdout=f) with fname.open("rt") as f: out = f.read().split() assert out == ["dsc_4664.jpg", "dsc_4831.jpg"] @@ -135,13 +135,13 @@ def test_rmtag_by_tag(imgdir, monkeypatch): """ monkeypatch.chdir(str(imgdir)) args = ["rmtag", "--tags", "Tokyo", "all"] - callscript("photoidx.py", args) + callscript("photo-idx.py", args) args = ["rmtag", "--tags", "Hakone", "all"] - callscript("photoidx.py", args) + callscript("photo-idx.py", args) fname = imgdir / "out" with fname.open("wt") as f: args = ["ls", "--tags", "all"] - callscript("photoidx.py", args, stdout=f) + callscript("photo-idx.py", args, stdout=f) with fname.open("rt") as f: out = f.read().split() assert out == ["dsc_5126.jpg", "dsc_5167.jpg"] @@ -152,11 +152,11 @@ def test_rmtag_all(imgdir, monkeypatch): """ monkeypatch.chdir(str(imgdir)) args = ["-d", str(imgdir), "rmtag", "all"] - callscript("photoidx.py", args) + callscript("photo-idx.py", args) fname = imgdir / "out" with fname.open("wt") as f: args = ["-d", str(imgdir), "ls", "--tags", "all"] - callscript("photoidx.py", args, stdout=f) + callscript("photo-idx.py", args, stdout=f) with fname.open("rt") as f: out = f.read().split() assert out == [] @@ -168,7 +168,7 @@ def test_ls_by_single_tag(imgdir): fname = imgdir / "out" with fname.open("wt") as f: args = ["-d", str(imgdir), "ls", "--tags", "Shinto_shrine"] - callscript("photoidx.py", args, stdout=f) + callscript("photo-idx.py", args, stdout=f) with fname.open("rt") as f: out = f.read().split() assert out == ["dsc_4664.jpg", "dsc_4831.jpg"] @@ -185,7 +185,7 @@ def test_ls_by_mult_tags(imgdir): fname = imgdir / "out" with fname.open("wt") as f: args = ["-d", str(imgdir), "ls", "--tags", "Tokyo,Shinto_shrine"] - callscript("photoidx.py", args, stdout=f) + callscript("photo-idx.py", args, stdout=f) with fname.open("rt") as f: out = f.read().split() assert out == ["dsc_4664.jpg"] @@ -203,7 +203,7 @@ def test_ls_by_neg_tags(imgdir, monkeypatch): fname = imgdir / "out" with fname.open("wt") as f: args = ["ls", "--tags", "Tokyo,!Shinto_shrine"] - callscript("photoidx.py", args, stdout=f) + callscript("photo-idx.py", args, stdout=f) with fname.open("rt") as f: out = f.read().split() assert out == ["dsc_4623.jpg"] @@ -220,7 +220,7 @@ def test_ls_by_empty_tag(imgdir): fname = imgdir / "out" with fname.open("wt") as f: args = ["-d", str(imgdir), "ls", "--tags", ""] - callscript("photoidx.py", args, stdout=f) + callscript("photo-idx.py", args, stdout=f) with fname.open("rt") as f: out = f.read().split() assert out == ["dsc_5126.jpg", "dsc_5167.jpg"] @@ -236,7 +236,7 @@ def test_ls_by_date_and_tag(imgdir): with fname.open("wt") as f: args = ["-d", str(imgdir), "ls", "--tags", "Tokyo", "--date", "2016-02-28"] - callscript("photoidx.py", args, stdout=f) + callscript("photo-idx.py", args, stdout=f) with fname.open("rt") as f: out = f.read().split() assert out == ["dsc_4623.jpg"] @@ -251,7 +251,7 @@ def test_lstags_all(imgdir): fname = imgdir / "out" with fname.open("wt") as f: args = ["-d", str(imgdir), "lstags"] - callscript("photoidx.py", args, stdout=f) + callscript("photo-idx.py", args, stdout=f) with fname.open("rt") as f: out = f.read().split() assert out == ["Hakone", "Shinto_shrine", "Tokyo"] @@ -266,7 +266,7 @@ def test_lstags_by_tags(imgdir, monkeypatch): fname = imgdir / "out" with fname.open("wt") as f: args = ["lstags", "--tags", "Tokyo"] - callscript("photoidx.py", args, stdout=f) + callscript("photo-idx.py", args, stdout=f) with fname.open("rt") as f: out = f.read().split() assert out == ["Shinto_shrine", "Tokyo"] @@ -277,11 +277,11 @@ def test_select_by_files(imgdir, monkeypatch): """ monkeypatch.chdir(str(imgdir)) args = ["select", "dsc_5126.jpg"] - callscript("photoidx.py", args) + callscript("photo-idx.py", args) fname = imgdir / "out" with fname.open("wt") as f: args = ["ls", "--selected"] - callscript("photoidx.py", args, stdout=f) + callscript("photo-idx.py", args, stdout=f) with fname.open("rt") as f: out = f.read().split() assert out == ["dsc_5126.jpg"] @@ -294,11 +294,11 @@ def test_select_by_tag(imgdir, monkeypatch): """ monkeypatch.chdir(str(imgdir)) args = ["select", "--tags", "Shinto_shrine"] - callscript("photoidx.py", args) + callscript("photo-idx.py", args) fname = imgdir / "out" with fname.open("wt") as f: args = ["ls", "--selected"] - callscript("photoidx.py", args, stdout=f) + callscript("photo-idx.py", args, stdout=f) with fname.open("rt") as f: out = f.read().split() assert out == ["dsc_4664.jpg", "dsc_4831.jpg", "dsc_5126.jpg"] @@ -309,11 +309,11 @@ def test_deselect_by_files(imgdir, monkeypatch): """ monkeypatch.chdir(str(imgdir)) args = ["deselect", "dsc_4831.jpg"] - callscript("photoidx.py", args) + callscript("photo-idx.py", args) fname = imgdir / "out" with fname.open("wt") as f: args = ["ls", "--selected"] - callscript("photoidx.py", args, stdout=f) + callscript("photo-idx.py", args, stdout=f) with fname.open("rt") as f: out = f.read().split() assert out == ["dsc_4664.jpg", "dsc_5126.jpg"] @@ -328,7 +328,7 @@ def test_stats_all(imgdir): fname = imgdir / "out" with fname.open("wt") as f: args = ["-d", str(imgdir), "stats"] - callscript("photoidx.py", args, stdout=f) + callscript("photo-idx.py", args, stdout=f) with fname.open("rt") as f: stats = yaml.safe_load(f) assert stats["Count"] == 5 @@ -357,7 +357,7 @@ def test_stats_filtered(imgdir): fname = imgdir / "out" with fname.open("wt") as f: args = ["-d", str(imgdir), "stats", "--tags", "Tokyo"] - callscript("photoidx.py", args, stdout=f) + callscript("photo-idx.py", args, stdout=f) with fname.open("rt") as f: stats = yaml.safe_load(f) assert stats["Count"] == 2 diff --git a/tests/test_05_checksum.py b/tests/test_05_checksum.py index 9807ea2..71d6510 100644 --- a/tests/test_05_checksum.py +++ b/tests/test_05_checksum.py @@ -5,7 +5,7 @@ import shutil import subprocess import pytest -import photo.index +import photoidx.index from conftest import tmpdir, gettestdata testimgs = [ @@ -27,7 +27,7 @@ def test_create_checksum(tmpdir): for fname in testimgfiles: shutil.copy(fname, str(tmpdir)) - with photo.index.Index(imgdir=tmpdir, hashalg=hashalg.keys()) as idx: + with photoidx.index.Index(imgdir=tmpdir, hashalg=hashalg.keys()) as idx: idx.write() @pytest.mark.dependency(depends=["test_create_checksum"]) @@ -37,7 +37,7 @@ def test_check_checksum(tmpdir, monkeypatch, alg): if not Path(checkprog).is_file(): pytest.skip("%s not found." % checkprog) fname = tmpdir / alg - with photo.index.Index(idxfile=tmpdir) as idx, fname.open("wt") as f: + with photoidx.index.Index(idxfile=tmpdir) as idx, fname.open("wt") as f: for i in idx: print("%s %s" % (i.checksum[alg], i.filename), file=f) monkeypatch.chdir(str(tmpdir)) @@ -47,8 +47,8 @@ def test_check_checksum(tmpdir, monkeypatch, alg): subprocess.check_call(cmd, stdin=f) def test_no_checksum(tmpdir): - with photo.index.Index(imgdir=tmpdir, hashalg=[]) as idx: + with photoidx.index.Index(imgdir=tmpdir, hashalg=[]) as idx: idx.write() - with photo.index.Index(idxfile=tmpdir) as idx: + with photoidx.index.Index(idxfile=tmpdir) as idx: for i in idx: assert i.checksum == {} diff --git a/tests/test_05_createupdate.py b/tests/test_05_createupdate.py index 11530d8..842d854 100644 --- a/tests/test_05_createupdate.py +++ b/tests/test_05_createupdate.py @@ -4,7 +4,7 @@ import filecmp import shutil import pytest -import photo.index +import photoidx.index from conftest import tmpdir, gettestdata testimgs = [ @@ -18,11 +18,11 @@ def test_createupdate(tmpdir): for fname in testimgfiles[:3]: shutil.copy(fname, str(tmpdir)) - with photo.index.Index(imgdir=tmpdir) as idx: + with photoidx.index.Index(imgdir=tmpdir) as idx: idx.write() for fname in testimgfiles[3:]: shutil.copy(fname, str(tmpdir)) - with photo.index.Index(idxfile=tmpdir, imgdir=tmpdir) as idx: + with photoidx.index.Index(idxfile=tmpdir, imgdir=tmpdir) as idx: idx.write() idxfile = str(tmpdir / ".index.yaml") assert filecmp.cmp(refindex, idxfile), "index file differs from reference" diff --git a/tests/test_05_legacy_md5.py b/tests/test_05_legacy_md5.py index e6e564a..54f574c 100644 --- a/tests/test_05_legacy_md5.py +++ b/tests/test_05_legacy_md5.py @@ -1,15 +1,15 @@ """Read legacy format index files. Since version 0.4 different checksum algorithms are supported (Issue -#12). As a consequence, the index file format is changed. -photo-tools still reads legacy files and transparently converts them -to the new format. This feature is tested in this module. +#12). As a consequence, the index file format is changed. photoidx +still reads legacy files and transparently converts them to the new +format. This feature is tested in this module. """ import filecmp import shutil import pytest -import photo.index +import photoidx.index from conftest import tmpdir, gettestdata testimgs = [ @@ -27,6 +27,6 @@ def test_legacyconvert(tmpdir): idxfile = str(tmpdir / ".index.yaml") shutil.copy(legacyindex, idxfile) # reading and writing the index transparantly converts it. - with photo.index.Index(idxfile=tmpdir) as idx: + with photoidx.index.Index(idxfile=tmpdir) as idx: idx.write() assert filecmp.cmp(idxfile, refindex), "index file differs from reference" diff --git a/tests/test_05_missing_exif.py b/tests/test_05_missing_exif.py index 1685348..e48f53d 100644 --- a/tests/test_05_missing_exif.py +++ b/tests/test_05_missing_exif.py @@ -8,7 +8,7 @@ import shutil import pytest -import photo.index +import photoidx.index from conftest import tmpdir, gettestdata testimgs = [ @@ -24,5 +24,5 @@ def imgdir(tmpdir): return tmpdir def test_create(imgdir): - with photo.index.Index(imgdir=imgdir) as idx: + with photoidx.index.Index(imgdir=imgdir) as idx: idx.write() diff --git a/tests/test_05_name.py b/tests/test_05_name.py index f362273..94eddcb 100644 --- a/tests/test_05_name.py +++ b/tests/test_05_name.py @@ -4,7 +4,7 @@ import filecmp import shutil import pytest -import photo.index +import photoidx.index from conftest import tmpdir, gettestdata refindex = gettestdata("index-name.yaml") @@ -17,7 +17,7 @@ def imgdir(tmpdir): def test_readwrite(imgdir): """Read the index file and write it out again. """ - with photo.index.Index(idxfile=imgdir) as idx: + with photoidx.index.Index(idxfile=imgdir) as idx: assert idx[0].name == "ginza.jpg" assert idx[1].name is None assert idx[3].name == "geisha.jpg" diff --git a/tests/test_05_reserved_tag.py b/tests/test_05_reserved_tag.py index fe3c5a5..caa1858 100644 --- a/tests/test_05_reserved_tag.py +++ b/tests/test_05_reserved_tag.py @@ -1,13 +1,13 @@ """Filter reserved tags on reading an index. -The prefix 'pidx:' for tags is reserved for internal use in -photo-tools. It should be removed when reading an index file. +The prefix 'pidx:' for tags is reserved for internal use in photoidx. +It should be removed when reading an index file. """ import filecmp import shutil import pytest -import photo.index +import photoidx.index from conftest import tmpdir, gettestdata testimgs = [ @@ -26,6 +26,6 @@ def test_reserved_tags_convert(tmpdir): shutil.copy(invindex, idxfile) # reading and writing the index transparantly filters out tags # using the reserved prefix. - with photo.index.Index(idxfile=tmpdir) as idx: + with photoidx.index.Index(idxfile=tmpdir) as idx: idx.write() assert filecmp.cmp(idxfile, refindex), "index file differs from reference" diff --git a/tests/test_05_stats.py b/tests/test_05_stats.py index 1005972..f828f3f 100644 --- a/tests/test_05_stats.py +++ b/tests/test_05_stats.py @@ -5,9 +5,9 @@ import shutil import pytest import yaml -import photo.index -import photo.idxfilter -from photo.stats import Stats +import photoidx.index +import photoidx.idxfilter +from photoidx.stats import Stats from conftest import tmpdir, gettestdata @@ -28,7 +28,7 @@ def imgdir(tmpdir): def test_stats_all(imgdir): """Get statistics on all images. """ - with photo.index.Index(idxfile=imgdir) as idx: + with photoidx.index.Index(idxfile=imgdir) as idx: stats = Stats(idx) assert stats.count == 5 assert stats.selected == 2 @@ -50,7 +50,7 @@ def test_stats_all(imgdir): def test_stats_all_yaml(imgdir): """The string representation of a Stats object is YAML. """ - with photo.index.Index(idxfile=imgdir) as idx: + with photoidx.index.Index(idxfile=imgdir) as idx: stats = yaml.safe_load(str(Stats(idx))) assert stats["Count"] == 5 assert stats["Selected"] == 2 @@ -72,8 +72,8 @@ def test_stats_all_yaml(imgdir): def test_stats_filtered(imgdir): """Get statistics on a selection of images. """ - with photo.index.Index(idxfile=imgdir) as idx: - idxfilter = photo.idxfilter.IdxFilter(tags="Tokyo") + with photoidx.index.Index(idxfile=imgdir) as idx: + idxfilter = photoidx.idxfilter.IdxFilter(tags="Tokyo") stats = Stats(idxfilter.filter(idx)) assert stats.count == 2 assert stats.selected == 1 diff --git a/tests/test_05_subdirs.py b/tests/test_05_subdirs.py index 9a3d4f2..7e4826e 100644 --- a/tests/test_05_subdirs.py +++ b/tests/test_05_subdirs.py @@ -12,9 +12,9 @@ import shutil import subprocess import pytest -import photo.index -import photo.idxfilter -from photo.geo import GeoPosition +import photoidx.index +import photoidx.idxfilter +from photoidx.geo import GeoPosition from conftest import tmpdir, gettestdata testimgs = { @@ -40,7 +40,7 @@ def imgdir(tmpdir): def test_create(imgdir): """Create the index. """ - with photo.index.Index(imgdir=imgdir) as idx: + with photoidx.index.Index(imgdir=imgdir) as idx: for k in ("Japan", "Quebec"): idx.extend_dir(imgdir / k) idx.write() @@ -57,7 +57,7 @@ def test_checksum(imgdir, monkeypatch): if not Path(checkprog).is_file(): pytest.skip("%s not found." % checkprog) fname = imgdir / hashalg - with photo.index.Index(idxfile=imgdir) as idx: + with photoidx.index.Index(idxfile=imgdir) as idx: with fname.open("wt") as f: for i in idx: print("%s %s" % (i.checksum[hashalg], i.filename), file=f) @@ -71,18 +71,18 @@ def test_checksum(imgdir, monkeypatch): def test_tag(imgdir, monkeypatch): """Test tagging of images. """ - with photo.index.Index(idxfile=imgdir) as idx: + with photoidx.index.Index(idxfile=imgdir) as idx: tokyo = GeoPosition("35.68 N, 139.77 E") - idxfilter = photo.idxfilter.IdxFilter(gpspos=tokyo, gpsradius=500.0) + idxfilter = photoidx.idxfilter.IdxFilter(gpspos=tokyo, gpsradius=500.0) for i in idxfilter.filter(idx): i.tags.add("Japan") quebec = GeoPosition("46.81 N, 71.22 W") - idxfilter = photo.idxfilter.IdxFilter(gpspos=quebec, gpsradius=500.0) + idxfilter = photoidx.idxfilter.IdxFilter(gpspos=quebec, gpsradius=500.0) for i in idxfilter.filter(idx): i.tags.add("Quebec") idx.write() - with photo.index.Index(idxfile=imgdir) as idx: + with photoidx.index.Index(idxfile=imgdir) as idx: for k in ("Japan", "Quebec"): - idxfilter = photo.idxfilter.IdxFilter(tags=k) + idxfilter = photoidx.idxfilter.IdxFilter(tags=k) fnames = [ i.filename for i in idxfilter.filter(idx) ] assert fnames == [ Path(k, f) for f in testimgs[k] ] diff --git a/tests/test_05_tagorder.py b/tests/test_05_tagorder.py index 5812240..15ca85b 100644 --- a/tests/test_05_tagorder.py +++ b/tests/test_05_tagorder.py @@ -11,8 +11,8 @@ import filecmp import shutil import pytest -import photo.index -import photo.idxfilter +import photoidx.index +import photoidx.idxfilter from conftest import tmpdir, gettestdata testimgs = [ @@ -51,11 +51,11 @@ def test_tag_ref(imgdir): idxfname = str(imgdir / ".index.yaml") reffname = str(imgdir / "index-ref.yaml") shutil.copy(gettestdata("index-create.yaml"), idxfname) - with photo.index.Index(idxfile=imgdir) as idx: + with photoidx.index.Index(idxfile=imgdir) as idx: taglist = [ "Japan", "Tokyo", "Hakone", "Kyoto", "Ginza", "Shinto_shrine", "Geisha", "Ryoan-ji" ] for t in taglist: - idxfilter = photo.idxfilter.IdxFilter(files=tags[t]) + idxfilter = photoidx.idxfilter.IdxFilter(files=tags[t]) for i in idxfilter.filter(idx): i.tags.add(t) idx.write() @@ -68,11 +68,11 @@ def test_tag_shuffle(imgdir): idxfname = str(imgdir / ".index.yaml") reffname = str(imgdir / "index-ref.yaml") shutil.copy(gettestdata("index-create.yaml"), idxfname) - with photo.index.Index(idxfile=imgdir) as idx: + with photoidx.index.Index(idxfile=imgdir) as idx: taglist = [ "Ginza", "Hakone", "Japan", "Geisha", "Shinto_shrine", "Tokyo", "Kyoto", "Ryoan-ji" ] for t in taglist: - idxfilter = photo.idxfilter.IdxFilter(files=tags[t]) + idxfilter = photoidx.idxfilter.IdxFilter(files=tags[t]) for i in idxfilter.filter(idx): i.tags.add(t) idx.write() @@ -85,7 +85,7 @@ def test_tag_remove(imgdir): idxfname = str(imgdir / ".index.yaml") reffname = str(imgdir / "index-ref.yaml") shutil.copy(gettestdata("index-create.yaml"), idxfname) - with photo.index.Index(idxfile=imgdir) as idx: + with photoidx.index.Index(idxfile=imgdir) as idx: taglist = [ "Tokyo", "Shinto_shrine", "Ginza", "Geisha", "Japan", "Ryoan-ji", "Hakone", "Kyoto" ] for t in taglist: @@ -105,13 +105,13 @@ def test_tag_extra(imgdir): idxfname = str(imgdir / ".index.yaml") reffname = str(imgdir / "index-ref.yaml") shutil.copy(gettestdata("index-create.yaml"), idxfname) - with photo.index.Index(idxfile=imgdir) as idx: + with photoidx.index.Index(idxfile=imgdir) as idx: taglist = [ "Japan", "Tokyo", "Hakone", "Kyoto", "Ginza", "Shinto_shrine", "Geisha", "Ryoan-ji" ] for i in idx: i.tags.add("extra") for t in taglist: - idxfilter = photo.idxfilter.IdxFilter(files=tags[t]) + idxfilter = photoidx.idxfilter.IdxFilter(files=tags[t]) for i in idxfilter.filter(idx): i.tags.add(t) for i in idx: diff --git a/tests/test_05_unicode_tags.py b/tests/test_05_unicode_tags.py index 63eacb9..2eff3e2 100644 --- a/tests/test_05_unicode_tags.py +++ b/tests/test_05_unicode_tags.py @@ -7,7 +7,7 @@ import filecmp import shutil import pytest -import photo.index +import photoidx.index from conftest import tmpdir, gettestdata testimgs = [ @@ -35,7 +35,7 @@ def imgdir(tmpdir): return tmpdir def test_tag_unicode(imgdir): - with photo.index.Index(imgdir=imgdir) as idx: + with photoidx.index.Index(imgdir=imgdir) as idx: for item in idx: for t in tags[str(item.filename)]: item.tags.add(t) diff --git a/tests/test_06_filelock.py b/tests/test_06_filelock.py index f1b6213..b5d882c 100644 --- a/tests/test_06_filelock.py +++ b/tests/test_06_filelock.py @@ -9,8 +9,8 @@ from multiprocessing import Process, Queue import shutil import pytest -import photo.index -import photo.idxfilter +import photoidx.index +import photoidx.idxfilter from conftest import tmpdir, gettestdata testimgs = [ @@ -44,15 +44,15 @@ def imgdir(tmpdir): def ls_bytag(imgdir, tag, qres, qwait): """List files by tag. """ - with photo.index.Index(idxfile=imgdir) as idx: - idxfilter = photo.idxfilter.IdxFilter(tags=tag) + with photoidx.index.Index(idxfile=imgdir) as idx: + idxfilter = photoidx.idxfilter.IdxFilter(tags=tag) qres.put((tag, [ str(i.filename) for i in idxfilter.filter(idx) ])) qwait.get() def add_tag(imgdir, tag, qres): """Add a tag to all items. """ - with photo.index.Index(idxfile=imgdir) as idx: + with photoidx.index.Index(idxfile=imgdir) as idx: for i in idx: i.tags.add(tag) try: @@ -114,7 +114,7 @@ def test_concurrent_read_write(imgdir): print("Writing process started.") # Verify that the writing process caught an AlreadyLockedError. r = qresw.get() - assert isinstance(r, photo.index.AlreadyLockedError) + assert isinstance(r, photoidx.index.AlreadyLockedError) print("Reply from writing process received.") # Now, allow the reading process to close the index file. qwait.put("done")