Skip to content

Commit

Permalink
Fix duplicate keys error.
Browse files Browse the repository at this point in the history
Change NestedText file suffix from nxt to nt.
Add documentation on exceptions.
  • Loading branch information
Ken Kundert authored and Ken Kundert committed Sep 1, 2020
1 parent 2ddf492 commit b5039ae
Show file tree
Hide file tree
Showing 32 changed files with 122 additions and 44 deletions.
59 changes: 59 additions & 0 deletions doc/api.rst
Expand Up @@ -27,3 +27,62 @@ exception there.

.. ignore the following (there is only one method, so no need for TOC)
.. autoclasstoc::
As with most exceptions, you can simply cast it to a string to get a reasonable
error message:

.. code-block:: python
>>> from textwrap import dedent
>>> import nestedtext
>>> content = dedent("""
... name:
... name:
... """)
>>> try:
... print(nestedtext.loads(content))
... except nestedtext.NestedTextError as e:
... print(str(e))
3: duplicate key: name.
You can also use the *report* method to print the message directly. This is
appropriate if you are using *inform* for your messaging as it follows
*inform*'s conventions.

.. code-block:: python
>> try:
.. print(nestedtext.loads(content))
.. except nestedtext.NestedTextError as e:
.. e.report()
The *terminate* method prints the message directly and exits.

.. code-block:: python
>> try:
.. print(nestedtext.loads(content))
.. except nestedtext.NestedTextError as e:
.. e.terminate()
Finally, exceptions produced by *NestedText* contain a *template* attribute that
contains the basic text of the message. You can change this message by
overriding the attribute when using *report*, *terminate*, or *render*.
*render* is like casting the exception to a string except that allows for the
passing of arguments. For example, to convert a particular message to Spanish,
you could use:

.. code-block:: python
>>> try:
... print(nestedtext.loads(content))
... except nestedtext.NestedTextError as e:
... template = None
... if e.template == 'duplicate key: {}.':
... template = 'llave duplicada: {}.'
... print(e.render(template=template))
3: llave duplicada: name.
18 changes: 9 additions & 9 deletions doc/format.rst
Expand Up @@ -58,7 +58,7 @@ newlines::
In strings, the initial '> ' is removed. Any spaces that follow would be
included in the string. For example.::

greeting: Dearest Katherine:
greeting: Dearest Kathy:
body:
> It has been such a long time. I am very much looking forward to
> seeing both you and Margaret again.
Expand Down Expand Up @@ -89,7 +89,7 @@ A value can also be a list or another dictionary::
> 2586 Marigold Land
> Topika, Kansas 20697
phone: 1-470-974-0398
email: margarett.hodge@uk.edu
email: margaret.hodge@uk.edu
kids:
- Arnie
- Zach
Expand All @@ -104,13 +104,13 @@ does not represent an issue. Only a hash as the first character on a line,
a leading dash-space or greater-space on a line, or the first non-quoted
colon-space are treated as special.

Multiline keys are not supported; a key must not contain a newline. In addition,
all keys in the same dictionary must be unique. If a key contains leading or
trailing spaces, a leading '- ' or '> ', or a ': ' anywhere in the key, you
should quote the key. Either single or double matching quotes may be used.
Single line string values should also be quoted in leading or trailing spaces
are significant, otherwise those spaces are removed. The quotes clarify the
extent of the value.
Multi-line keys are not supported; a key must not contain a newline. In
addition, all keys in the same dictionary must be unique. If a key contains
leading or trailing spaces, a leading '- ' or '> ', or a ': ' anywhere in the
key, you should quote the key. Either single or double matching quotes may be
used. Single line string values should also be quoted in leading or trailing
spaces are significant, otherwise those spaces are removed. The quotes clarify
the extent of the value.
For example::

sep: ' — '
Expand Down
4 changes: 2 additions & 2 deletions doc/index.rst
Expand Up @@ -41,7 +41,7 @@ Here is an example of a file that contains a few addresses::
> 2586 Marigold Lane
> Topika, Kansas 20682
phone: 1-470-974-0398
email: margarett.hodge@uk.edu
email: margaret.hodge@uk.edu
kids:
- Arnie
- Zach
Expand Down Expand Up @@ -76,7 +76,7 @@ Issues
------

Please ask questions or report problems on `Github
<https://github.com/KenKundert/quantiphy/issues>`_.
<https://github.com/KenKundert/nestedtext/issues>`_.


Contributing
Expand Down
4 changes: 2 additions & 2 deletions doc/writer.rst
Expand Up @@ -65,8 +65,8 @@ dictionaries, lists, and strings to *NestedText*:
> rosebud replicate freshen javelin abbot autocue beater byway
This example writes to a string, but it is common to write to a file. The file
name and extension are arbitrary. However, the convention is to use a '.nxt'
extension with *NestedText* files.
name and extension are arbitrary. However, by convention a '.nt' suffix is
generally used for *NestedText* files.

There are several mechanisms available for handling objects that are otherwise
unsupported by the format.
Expand Down
8 changes: 4 additions & 4 deletions nestedtext.py
Expand Up @@ -248,12 +248,12 @@ def read_dict(lines, depth):
indentation_error(line, depth)
if line.kind != "dict item":
report("expected dictionary item", line)
if line.key in data:
report('duplicate key: {}.', line, line.key)
if '"' in line.key and "'" in line.key:
report("""key must not contain both " and '.""", line, line.key)
if line.value:
# dbg(line, 'dv')
if line.key in data:
report('duplicate key: {}.', line, line.key)
if '"' in line.key and "'" in line.key:
report("""key must not contain both " and '.""", line, line.key)
data.update({line.key: line.value})
else:
# dbg(line, 'd↵')
Expand Down
17 changes: 17 additions & 0 deletions test_nestedtext.py
Expand Up @@ -519,6 +519,23 @@ def test_loads_errors():
assert exception.value.line == ' \t > first line'
assert exception.value.loc == 4

content = dedent("""
name :
name :
""")
with pytest.raises(nestedtext.NestedTextError) as exception:
nestedtext.loads(content)
assert str(exception.value) == '3: duplicate key: name.'
assert exception.value.args == ('name',)
assert exception.value.kwargs == dict(
culprit = (3,),
codicil = ('«name :»',),
line = 'name :',
template = 'duplicate key: {}.',
)
assert exception.value.line == 'name :'
assert exception.value.render(template='llave duplicada: {}.') == '3: llave duplicada: name.'

# test_dump() {{{1
def test_dump():
data = {'peach': 3, 'apricot': 8, 'blueberry': '1 lb', 'orange': 4}
Expand Down
14 changes: 7 additions & 7 deletions tests/README.rst
Expand Up @@ -16,27 +16,27 @@ status of 1.

An external encoder must accept *JSON* as input and write *NestedText* as
output. An 'external' encoder that actually uses the reference encoder is
provided in *json-to-nxt*
provided in *json-to-nestedtext*

An external decoder must accept *nxt* as input and write *JSON* as output.
An 'external' decoder that actually uses the reference decoder is provided in
*nxt-to-json*
An external decoder must accept *NestedText* as input and write *JSON* as
output. An 'external' decoder that actually uses the reference decoder is
provided in *nestedtext-to-json*

For example, to run all tests on internal reference encoder and decoder::

$ ./run-tests

To run tests on an external encoder use::

$ ./run-tests --encoder=json-to-nxt
$ ./run-tests --encoder=json-to-nestedtext

To run tests on an external decoder use::

$ ./run-tests --decoder=nxt-to-json
$ ./run-tests --decoder=nestedtext-to-json

To run tests on both an external encoder and decoder use::

$ ./run-tests --encoder=json-to-nxt --decoder=nxt-to-json
$ ./run-tests --encoder=json-to-nestedtext --decoder=nestedtext-to-json

Tests
-----
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
2 changes: 2 additions & 0 deletions tests/invalid/dict7.nt
@@ -0,0 +1,2 @@
name :
name :
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
4 changes: 2 additions & 2 deletions tests/json-to-nxt → tests/json-to-nestedtext
Expand Up @@ -3,7 +3,7 @@
Read a JSON file and convert it to NestedText.
usage:
json-to-nxt [options] [<filename>]
json-to-nestedtext [options] [<filename>]
options:
-f, --force force overwrite of output file
Expand Down Expand Up @@ -31,7 +31,7 @@ try:
data = json.loads(json_content)
nestedtext_content = nestedtext.dumps(data)
if input_filename:
output_path = input_path.with_suffix('.nxt')
output_path = input_path.with_suffix('.nt')
if output_path.exists():
if not cmdline['--force']:
fatal('file exists, use -f to force over-write.', culprit=output_path)
Expand Down
8 changes: 4 additions & 4 deletions tests/nxt-to-json → tests/nestedtext-to-json
Expand Up @@ -3,7 +3,7 @@
Read a NestedText file and convert it to JSON.
usage:
nxt-to-json [options] [<filename>]
nestedtext-to-json [options] [<filename>]
options:
-f, --force force overwrite of output file
Expand All @@ -25,10 +25,10 @@ input_filename = cmdline['<filename>']
try:
if input_filename:
input_path = Path(input_filename)
nxt_content = input_path.read_text()
nestedtext_content = input_path.read_text()
else:
nxt_content = sys.stdin.read()
data = nestedtext.loads(nxt_content, input_filename)
nestedtext_content = sys.stdin.read()
data = nestedtext.loads(nestedtext_content, input_filename)
json_content = json.dumps(data, indent=4)
if input_filename:
output_path = input_path.with_suffix('.json')
Expand Down
28 changes: 14 additions & 14 deletions tests/run-tests
Expand Up @@ -39,12 +39,12 @@ shlib_set_prefs(use_inform=True)
# gather tests
valid_testcases = [
case.with_suffix('')
for case in list(lsf('valid', select='*.nxt')) + list(lsf('valid',
for case in list(lsf('valid', select='*.nt')) + list(lsf('valid',
select='*.json'))
]
invalid_testcases = [
case.with_suffix('')
for case in list(lsf('invalid', select='*.nxt')) + list(lsf('invalid',
for case in list(lsf('invalid', select='*.nt')) + list(lsf('invalid',
select='*.json'))
]
testcases = set()
Expand All @@ -64,25 +64,25 @@ def load_json_file(case):
data = json.loads(json_content)
return data

def load_nxt_file(case):
nxt_path = case.with_suffix('.nxt')
nxt_content = nxt_path.read_text()
data = nestedtext.loads(nxt_content, nxt_path)
def load_nestedtext_file(case):
nestedtext_path = case.with_suffix('.nt')
nestedtext_content = nestedtext_path.read_text()
data = nestedtext.loads(nestedtext_content, nestedtext_path)
return data

def run_internal_encoder_test(case):
given_data = load_json_file(case)
try:
nxt_content = nestedtext.dumps(given_data)
nestedtext_content = nestedtext.dumps(given_data)
except nestedtext.NestedTextError:
return 'error'
extracted_data = nestedtext.loads(nxt_content)
extracted_data = nestedtext.loads(nestedtext_content)
success = extracted_data == given_data
return None if success else 'mismatch'

def run_internal_decoder_test(case):
try:
extracted_data = load_nxt_file(case)
extracted_data = load_nestedtext_file(case)
except nestedtext.NestedTextError:
return 'error'
expected_data = load_json_file(case)
Expand All @@ -94,14 +94,14 @@ def run_external_encoder_test(encoder, path):
process = Run(encoder, stdin=given_json_content, modes='sOEW1')
if process.status != 0:
return 'error'
nxt_content = process.stdout
extracted_data = nestedtext.loads(nxt_content)
nestedtext_content = process.stdout
extracted_data = nestedtext.loads(nestedtext_content)
success = extracted_data == given_data
return None if success else 'mismatch'

def run_external_decoder_test(decoder, path):
given_nxt_content = path.read_text()
process = Run(encoder, stdin=given_nxt_content, modes='sOEW1')
given_nestedtext_content = path.read_text()
process = Run(encoder, stdin=given_nestedtext_content, modes='sOEW1')
if process.status != 0:
return error
json_content = process.stdout
Expand Down Expand Up @@ -130,7 +130,7 @@ with Inform(verbose=cmdline['--verbose']):
if fails != 'error':
error('encoder fails to fail.', culprit=src_file)

src_file = case.with_suffix('.nxt')
src_file = case.with_suffix('.nt')
if decoder and src_file.exists():
comment(src_file, 'decoder')
tests += 1
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.

0 comments on commit b5039ae

Please sign in to comment.