Skip to content

Commit ad76f3e

Browse files
authored
Release 1.2.0 (#32)
* Add deep test object * Add storage root tests for E072, E072, E003d, E004a, E004b (#27) * Add standalone inventory validation (#31)
1 parent 4fe7f97 commit ad76f3e

File tree

9 files changed

+127
-30
lines changed

9 files changed

+127
-30
lines changed

CHANGES.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# ocfl-py changelog
22

3+
## 2021-03-24 v1.2.0
4+
5+
* Add ability for `ocfl-validate.py` to validate a standalone inventory
6+
37
## 2020-09-22 v1.1.1
48

59
* Add deeply nested text object (`extra_fixtures/good-objects/ten_level_deep_directories.zip`)

README

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
ocfl-py - An OCFL implementation in Python
33
==========================================
44

5-
.. image:: https://travis-ci.org/zimeon/ocfl-py.svg?branch=main
6-
:target: https://travis-ci.org/zimeon/ocfl-py
5+
.. image:: https://travis-ci.com/zimeon/ocfl-py.svg?branch=main
6+
:target: https://travis-ci.com/zimeon/ocfl-py
77
.. image:: https://coveralls.io/repos/github/zimeon/ocfl-py/badge.svg?branch=main
88
:target: https://coveralls.io/github/zimeon/ocfl-py?branch=main
99

@@ -19,7 +19,7 @@ Installing
1919

2020
This code requires Python 3.
2121

22-
This code attempts to support the OCFL specification v1.0 and to additional
22+
This code attempts to support the OCFL specification v1.0 and additional
2323
developments. To get the most up to date version check out the ``main``
2424
branch from github (or if you are reckless you can try the ``develop`` branch).
2525

@@ -34,7 +34,7 @@ Use
3434

3535
There should then be three scripts available:
3636

37-
- ``ocfl-validate.py`` - validate OCFL objects or OCFL storage roots
37+
- ``ocfl-validate.py`` - validate OCFL objects, OCFL storage roots or standalone OCFL inventory files
3838
- ``ocfl-object.py`` - build, manipulate, extract from or validate an OCFL object
3939
- ``ocfl-store.py`` - add or access OCFL objects under an OCFL storage root
4040

@@ -59,7 +59,7 @@ for guidelines for contributing.
5959
Copyright and License
6060
---------------------
6161

62-
Copyright 2018--2020 Simeon Warner and `contributors
62+
Copyright 2018--2021 Simeon Warner and `contributors
6363
<https://github.com/zimeon/ocfl-py/graphs/contributors>`_.
6464
Provided under the MIT license, see `LICENSE.txt
6565
<https://github.com/zimeon/ocfl-py/blob/main/LICENSE.txt>`_.

ocfl-validate.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@
66
import sys
77

88
parser = argparse.ArgumentParser(
9-
description='Validate one or more OCFL objects or storage roots. By default '
10-
'shows any errors or warnings, and final validation status. Use -q to show '
11-
'only errors, -Q to show only validation status.')
9+
description='Validate one or more OCFL objects, storage roots or standalone '
10+
'inventory files. By default shows any errors or warnings, and final '
11+
'validation status. Use -q to show only errors, -Q to show only validation '
12+
'status.')
1213
parser.add_argument('path', type=str, nargs='*',
13-
help='OCFL object or storage root path(s) to validate')
14+
help='OCFL object, storage root or inventory path(s) to validate')
1415
parser.add_argument('--quiet', '-q', action='store_true',
1516
help="Be quiet, do not show warnings")
1617
parser.add_argument('--very-quiet', '-Q', action='store_true',
@@ -34,25 +35,33 @@
3435
num = 0
3536
num_good = 0
3637
num_paths = len(args.path)
38+
show_warnings = not args.quiet and not args.very_quiet
3739
for path in args.path:
3840
num += 1
3941
path_type = ocfl.find_path_type(path)
4042
if path_type == 'object':
4143
log.info("Validating OCFL Object at " + path)
4244
obj = ocfl.Object(lax_digests=args.lax_digests)
4345
if obj.validate(path,
44-
show_warnings=not args.quiet and not args.very_quiet,
46+
show_warnings=show_warnings,
4547
show_errors=not args.very_quiet,
4648
check_digests=not args.no_check_digests):
4749
num_good += 1
4850
elif path_type == 'root':
4951
log.info("Validating OCFL Storage Root at " + path)
5052
store = ocfl.Store(root=path,
5153
lax_digests=args.lax_digests)
52-
if store.validate(show_warnings=not args.quiet and not args.very_quiet,
54+
if store.validate(show_warnings=show_warnings,
5355
show_errors=not args.very_quiet,
5456
check_digests=not args.no_check_digests):
5557
num_good += 1
58+
elif path_type == 'file':
59+
log.info("Validating separate OCFL Inventory at " + path)
60+
obj = ocfl.Object(lax_digests=args.lax_digests)
61+
if obj.validate_inventory(path,
62+
show_warnings=show_warnings,
63+
show_errors=not args.very_quiet):
64+
num_good += 1
5665
else:
5766
log.error("Bad path %s (%s)" % (path, path_type))
5867
if num_paths > 1:

ocfl/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
"""Version number for this Python implementation of OCFL."""
2-
__version__ = '1.1.1'
2+
__version__ = '1.2.0'

ocfl/object.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"""Core of OCFL Object library."""
33
import copy
44
import fs
5+
import fs.path
56
import fs.copy
67
import hashlib
78
import json
@@ -19,7 +20,7 @@
1920
from .object_utils import remove_first_directory, make_unused_filepath, next_version
2021
from .pyfs import open_fs
2122
from .namaste import Namaste
22-
from .validator import Validator
23+
from .validator import Validator, ValidatorAbortException
2324
from .version_metadata import VersionMetadata
2425

2526
INVENTORY_FILENAME = 'inventory.json'
@@ -540,6 +541,28 @@ def validate(self, objdir, show_warnings=True, show_errors=True, check_digests=T
540541
self.log.info("OCFL object at %s is INVALID" % (objdir))
541542
return passed
542543

544+
def validate_inventory(self, path, show_warnings=True, show_errors=True):
545+
"""Validate just an OCFL Object inventory at path."""
546+
validator = Validator(show_warnings=show_warnings,
547+
show_errors=show_errors)
548+
try:
549+
(inv_dir, inv_file) = fs.path.split(path)
550+
validator.obj_fs = open_fs(inv_dir, create=False)
551+
validator.validate_inventory(inv_file, where='standalone')
552+
except fs.errors.ResourceNotFound:
553+
validator.log.error('E033', where='standalone', explanation='failed to open directory')
554+
except ValidatorAbortException:
555+
pass
556+
passed = (validator.log.num_errors == 0)
557+
messages = str(validator)
558+
if messages != '':
559+
print(messages)
560+
if passed:
561+
self.log.info("Standalone OCFL inventory at %s is VALID" % (path))
562+
else:
563+
self.log.info("Standalone OCFL inventory at %s is INVALID" % (path))
564+
return passed
565+
543566
def extract(self, objdir, version, dstdir):
544567
"""Extract version from object at objdir into dstdir.
545568

ocfl/object_utils.py

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# -*- coding: utf-8 -*-
22
"""Utility functions to support the OCFL Object library."""
33
import fs
4+
import fs.path
45
import os
5-
import os.path
66
import re
77
import sys
88
try:
@@ -50,7 +50,7 @@ def add_shared_args(parser):
5050
def check_shared_args(args):
5151
"""Check arguments set with add_shared_args."""
5252
if args.version:
53-
print("%s is part of ocfl-py version %s" % (os.path.basename(sys.argv[0]), __version__))
53+
print("%s is part of ocfl-py version %s" % (fs.path.basename(sys.argv[0]), __version__))
5454
sys.exit(0)
5555

5656

@@ -85,12 +85,12 @@ def remove_first_directory(path):
8585
# split and rejoins, excluding the first directory
8686
rpath = ''
8787
while True:
88-
(head, tail) = os.path.split(path)
88+
(head, tail) = fs.path.split(path)
8989
if head == path or tail == path:
9090
break
9191
else:
9292
path = head
93-
rpath = tail if rpath == '' else os.path.join(tail, rpath)
93+
rpath = tail if rpath == '' else fs.path.join(tail, rpath)
9494
return rpath
9595

9696

@@ -105,15 +105,34 @@ def make_unused_filepath(filepath, used, separator='__'):
105105

106106

107107
def find_path_type(path):
108-
"""Return a string indicating the type of thing, object or root, at path.
108+
"""Return a string indicating the type of thing at the given path.
109109
110-
Looks only at "0=*" Namaste files to determing the path type. Will return
111-
an error description if not an `object` or storage `root`.
110+
Return values:
111+
'root' - looks like an OCFL Storage Root
112+
'object' - looks like an OCFL Object
113+
'file' - a file, might be an inventory
114+
other string explains error description
115+
116+
Looks only at "0=*" Namaste files to determine the directory type.
112117
"""
113118
try:
114119
pyfs = open_fs(path, create=False)
115120
except (fs.opener.errors.OpenerError, fs.errors.CreateFailed) as e:
116-
return("does not exist or cannot be opened (" + str(e) + ")")
121+
# Failed to open path as a filesystem, try enclosing directory
122+
# in case path is a file
123+
(parent, filename) = fs.path.split(path)
124+
try:
125+
pyfs = open_fs(parent, create=False)
126+
except (fs.opener.errors.OpenerError, fs.errors.CreateFailed) as e:
127+
return("path cannot be opened, and nor can parent (" + str(e) + ")")
128+
# Can open parent, is filename a file there?
129+
try:
130+
info = pyfs.getinfo(filename)
131+
except fs.errors.ResourceNotFound:
132+
return("path does not exist")
133+
if info.is_dir:
134+
return("directory that could not be opened as a filesystem, this should not happen") # pragma: no cover
135+
return('file')
117136
namastes = find_namastes(0, pyfs=pyfs)
118137
if len(namastes) == 0:
119138
return("no 0= declaration file")

tests/test_namaste.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,14 @@ def test15_check_content(self):
9191
self.assertRaises(NamasteException, Namaste().check_content, 'tests/testdata/namaste')
9292
self.assertRaises(NamasteException, Namaste(0, 'a').check_content, 'tests/testdata/namaste/does_not_exist')
9393
self.assertRaises(NamasteException, Namaste(0, 'bison').check_content, 'tests/testdata/namaste')
94+
# Using pyfs...
95+
tmpfs = fs.tempfs.TempFS()
96+
tmpfs.writetext('9=niner', 'niner\n')
97+
tmpfs.writetext('8=smiley', 'FROWNY\n')
98+
Namaste(9, 'niner').check_content(pyfs=tmpfs)
99+
Namaste(9, 'niner').check_content(dir='', pyfs=tmpfs)
100+
self.assertRaises(NamasteException, Namaste(8, 'smiley').check_content, pyfs=tmpfs)
101+
self.assertRaises(NamasteException, Namaste(7, 'not-there').check_content, pyfs=tmpfs)
94102

95103
def test16_content_ok(self):
96104
"""Test content_ok method."""

tests/test_object.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ def assertRegex(self, *args, **kwargs):
2121
"""Hack for Python 2.7."""
2222
return self.assertRegexpMatches(*args, **kwargs)
2323

24-
def test01_init(self):
24+
def test00_init(self):
2525
"""Test Object init."""
2626
oo = Object()
2727
self.assertEqual(oo.id, None)
@@ -33,6 +33,14 @@ def test01_init(self):
3333
self.assertEqual(oo.digest_algorithm, 'sha1')
3434
self.assertEqual(oo.fixity, ['md5', 'crc16'])
3535

36+
def test01_open_fs(self):
37+
"""Test open_fs."""
38+
oo = Object()
39+
self.assertEqual(oo.obj_fs, None)
40+
oo.open_fs('tests')
41+
self.assertNotEqual(oo.obj_fs, None)
42+
self.assertRaises(ObjectException, oo.open_fs, 'tests/testdata/i_do_not_exist')
43+
3644
def test02_parse_version_directory(self):
3745
"""Test parse_version_directory."""
3846
oo = Object()
@@ -304,7 +312,7 @@ def test12_show(self):
304312
self.assertTrue('├── 0=ocfl_object_1.0' in out)
305313
# FIXME - need real tests in here when there is real output
306314

307-
def test13_validate(self):
315+
def test_validate(self):
308316
"""Test validate method."""
309317
oo = Object()
310318
self.assertTrue(oo.validate(objdir='fixtures/1.0/good-objects/minimal_one_version_one_file'))
@@ -313,7 +321,16 @@ def test13_validate(self):
313321
self.assertFalse(oo.validate(objdir='fixtures/1.0/bad-objects/E001_no_decl'))
314322
self.assertFalse(oo.validate(objdir='fixtures/1.0/bad-objects/E036_no_id'))
315323

316-
def test14_parse_inventory(self):
324+
def test_validate_inventory(self):
325+
"""Test validate_inventory method."""
326+
oo = Object()
327+
self.assertTrue(oo.validate_inventory(path='fixtures/1.0/good-objects/minimal_one_version_one_file/inventory.json'))
328+
# Error cases
329+
self.assertFalse(oo.validate_inventory(path='tests/testdata/i_do_not_exist'))
330+
self.assertFalse(oo.validate_inventory(path='fixtures/1.0/bad-objects/E036_no_id/inventory.json'))
331+
self.assertFalse(oo.validate_inventory(path='tests/testdata//namaste/0=frog')) # not JSON
332+
333+
def test_parse_inventory(self):
317334
"""Test parse_inventory method."""
318335
oo = Object()
319336
oo.open_fs('fixtures/1.0/good-objects/minimal_one_version_one_file')
@@ -336,7 +353,7 @@ def test14_parse_inventory(self):
336353
oo.open_fs('fixtures/1.0/bad-objects/E036_no_id')
337354
self.assertRaises(ObjectException, oo.parse_inventory)
338355

339-
def test15_map_filepath(self):
356+
def test_map_filepath(self):
340357
"""Test map_filepath method."""
341358
oo = Object()
342359
# default is uri
@@ -353,7 +370,7 @@ def test15_map_filepath(self):
353370
oo.filepath_normalization = '???'
354371
self.assertRaises(Exception, oo.map_filepath, 'a', 'v1', {})
355372

356-
def test16_extract(self):
373+
def test_extract(self):
357374
"""Test extract method."""
358375
tempdir = tempfile.mkdtemp(prefix='test_extract')
359376
oo = Object()

tests/test_object_utils.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"""Object tests."""
33
import argparse
44
import unittest
5-
from ocfl.object_utils import remove_first_directory, make_unused_filepath, next_version, add_object_args, find_path_type
5+
from ocfl.object_utils import remove_first_directory, make_unused_filepath, next_version, add_object_args, add_shared_args, check_shared_args, find_path_type
66

77

88
class TestAll(unittest.TestCase):
@@ -48,12 +48,29 @@ def test_add_object_args(self):
4848
args = parser.parse_args(['--skip', 'aa'])
4949
self.assertIn('aa', args.skip)
5050

51+
def test_add_shared_args(self):
52+
"""Test (kinda) adding shared args."""
53+
parser = argparse.ArgumentParser()
54+
add_shared_args(parser)
55+
args = parser.parse_args(['--version', '-v'])
56+
self.assertTrue(args.version)
57+
self.assertTrue(args.verbose)
58+
59+
def test_check_shared_args(self):
60+
"""Test check of shared args."""
61+
parser = argparse.ArgumentParser()
62+
add_shared_args(parser)
63+
args = parser.parse_args(['--version', '-v'])
64+
check_shared_args(parser.parse_args(['-v']))
65+
self.assertRaises(SystemExit, check_shared_args, parser.parse_args(['--version']))
66+
5167
def test_find_path_type(self):
5268
"""Test find_path_type function."""
69+
self.assertEqual(find_path_type("extra_fixtures/good-storage-roots/fedora-root"), "root")
70+
self.assertEqual(find_path_type("fixtures/1.0/good-objects/minimal_one_version_one_file"), "object")
71+
self.assertEqual(find_path_type("README"), "file")
5372
self.assertIn("does not exist", find_path_type("this_path_does_not_exist"))
54-
self.assertIn("cannot be opened", find_path_type("ocfl-object.py"))
73+
self.assertIn("nor can parent", find_path_type("still_nope/nope_doesnt_exist"))
5574
self.assertEqual(find_path_type("ocfl"), "no 0= declaration file")
5675
self.assertIn("more than one 0= declaration file", find_path_type("extra_fixtures/misc/multiple_declarations"))
5776
self.assertIn("unrecognized", find_path_type("extra_fixtures/misc/unknown_declaration"))
58-
self.assertEqual(find_path_type("extra_fixtures/good-storage-roots/fedora-root"), "root")
59-
self.assertEqual(find_path_type("fixtures/1.0/good-objects/minimal_one_version_one_file"), "object")

0 commit comments

Comments
 (0)