Skip to content

Commit d4f9906

Browse files
committed
Added --json to CLI
1 parent 8d9985c commit d4f9906

File tree

5 files changed

+132
-55
lines changed

5 files changed

+132
-55
lines changed

README.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ Options:
3939
-v, --version Show the version and exit.
4040
-w, --walk Walk directory tree, processing each file in the
4141
tree
42+
-j, --json Print output in JSON format, for use with --list
43+
and --get.
4244
--set ATTRIBUTE VALUE Set ATTRIBUTE to VALUE
4345
--list List all metadata attributes for FILE
4446
--clear ATTRIBUTE Remove attribute from FILE
@@ -65,7 +67,7 @@ set keywords to ['foo', 'bar']
6567
6668
Short Name Description
6769
authors kMDItemAuthors, com.apple.metadata:kMDItemAuthors; The
68-
author, or authors, of the contents of the file. An array of
70+
author, or authors, of the contents of the file. A list of
6971
strings.
7072
comment kMDItemComment, com.apple.metadata:kMDItemComment; A comment
7173
related to the file. This differs from the Finder comment,
@@ -94,15 +96,15 @@ keywords kMDItemKeywords, com.apple.metadata:kMDItemKeywords;
9496
Keywords associated with this file. For example, “Birthday”,
9597
“Important”, etc. This differs from Finder tags
9698
(_kMDItemUserTags) which are keywords/tags shown in the
97-
Finder and searchable in Spotlight using "tag:tag_name"An
98-
array of strings.
99+
Finder and searchable in Spotlight using "tag:tag_name"A
100+
list of strings.
99101
tags _kMDItemUserTags, com.apple.metadata:_kMDItemUserTags;
100102
Finder tags; searchable in Spotlight using "tag:tag_name".
101103
If you want tags/keywords visible in the Finder, use this
102-
instead of kMDItemKeywords.
104+
instead of kMDItemKeywords. A list of strings.
103105
wherefroms kMDItemWhereFroms, com.apple.metadata:kMDItemWhereFroms;
104106
Describes where the file was obtained from (e.g. URL
105-
downloaded from). An array of strings.
107+
downloaded from). A list of strings.
106108
```
107109

108110

osxmetadata/__init__.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import xattr
1818

1919
from .attributes import ATTRIBUTES, Attribute
20-
from .classes import _AttributeList, _AttributeTagsList
20+
from .classes import _AttributeList, _AttributeTagsList
2121
from .constants import ( # _DOWNLOAD_DATE,; _FINDER_COMMENT,; _TAGS,; _WHERE_FROM,
2222
_COLORIDS,
2323
_COLORNAMES,
@@ -46,9 +46,11 @@
4646
clear_finder_comment,
4747
validate_attribute_value,
4848
)
49+
from ._version import __version__
4950

5051
__all__ = [
5152
"OSXMetaData",
53+
"__version__",
5254
"ATTRIBUTES",
5355
"kMDItemAuthors",
5456
"kMDItemComment",
@@ -127,7 +129,8 @@ def get_attribute(self, attribute_name):
127129

128130
# user tags need special processing to normalize names
129131
if attribute.name == "tags":
130-
return self.tags
132+
self.tags._load_data()
133+
return self.tags.data
131134

132135
try:
133136
plist = plistlib.loads(self._attrs[attribute.constant])
@@ -167,7 +170,7 @@ def set_attribute(self, attribute_name, value):
167170

168171
# verify type is correct
169172
value = validate_attribute_value(attribute, value)
170-
173+
171174
# if attribute.list and (type(value) == list or type(value) == set):
172175
# for val in value:
173176
# if attribute.type_ != type(val):
@@ -219,7 +222,7 @@ def append_attribute(self, attribute_name, value, update=False):
219222
# start with existing values
220223
new_value = self.get_attribute(attribute.name)
221224

222-
value = validate_attribute_value(attribute,value)
225+
value = validate_attribute_value(attribute, value)
223226

224227
if attribute.list:
225228
if new_value is not None:
@@ -240,8 +243,7 @@ def append_attribute(self, attribute_name, value, update=False):
240243
if new_value is not None:
241244
new_value += value
242245
else:
243-
new_value = value
244-
246+
new_value = value
245247

246248
# # verify type is correct
247249
# if attribute.list and (type(value) == list or type(value) == set):

osxmetadata/__main__.py

Lines changed: 60 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
from ._version import __version__
1616
from .attributes import _LONG_NAME_WIDTH, _SHORT_NAME_WIDTH, ATTRIBUTES
17+
from .classes import _AttributeList, _AttributeTagsList
1718
from .constants import _TAGS_NAMES
1819
from .utils import validate_attribute_value
1920

@@ -106,9 +107,8 @@ def get_help(self, ctx):
106107
"-j",
107108
"json_",
108109
is_flag=True,
109-
help="Print output in JSON format, for use with --list.",
110+
help="Print output in JSON format, for use with --list and --get.",
110111
default=False,
111-
hidden=True, # not yet implemented
112112
)
113113
DEBUG_OPTION = click.option(
114114
"--debug", required=False, is_flag=True, default=False, hidden=True
@@ -218,7 +218,14 @@ def cli(
218218
if invalid_attr:
219219
click.echo("") # add a new line before rest of help text
220220
click.echo(ctx.get_help())
221-
ctx.exit()
221+
ctx.exit(2)
222+
223+
# check that json_ only used with get or list_
224+
if json_ and not any([get, list_]):
225+
click.echo("--json can only be used with --get or --list", err=True)
226+
click.echo("") # add a new line before rest of help text
227+
click.echo(ctx.get_help())
228+
ctx.exit(2)
222229

223230
for f in files:
224231
if walk and os.path.isdir(f):
@@ -319,59 +326,69 @@ def process_file(fpath, json_, set_, append, update, remove, clear, get, list_):
319326

320327
if get:
321328
logging.debug(f"get: {get}")
329+
if json_:
330+
data = {}
331+
data["_version"] = __version__
332+
data["_filepath"] = str(fpath)
333+
data["_filename"] = fpath.name
322334
for attr in get:
323335
attribute = ATTRIBUTES[attr]
324336
logging.debug(f"getting {attr}")
325-
value = md.get_attribute(attribute.name)
326-
click.echo(
327-
f"{attribute.name:{_SHORT_NAME_WIDTH}}{attribute.constant:{_LONG_NAME_WIDTH}} = {value}"
328-
)
337+
if json_:
338+
if attribute.type_ == datetime.datetime:
339+
# need to convert datetime.datetime to string to serialize
340+
value = md.get_attribute(attribute.name)
341+
if type(value) == list:
342+
value = [v.isoformat() for v in value]
343+
else:
344+
value = value.isoformat()
345+
data[attribute.constant] = value
346+
else:
347+
# get raw value
348+
data[attribute.constant] = md.get_attribute(attribute.name)
349+
else:
350+
value = md.get_attribute_str(attribute.name)
351+
click.echo(
352+
f"{attribute.name:{_SHORT_NAME_WIDTH}}{attribute.constant:{_LONG_NAME_WIDTH}} = {value}"
353+
)
354+
if json_:
355+
json_str = json.dumps(data)
356+
click.echo(json_str)
329357

330358
if list_:
331359
attribute_list = md.list_metadata()
360+
if json_:
361+
data = {}
362+
data["_version"] = __version__
363+
data["_filepath"] = str(fpath)
364+
data["_filename"] = fpath.name
332365
for attr in attribute_list:
333366
try:
334367
attribute = ATTRIBUTES[attr]
335-
value = md.get_attribute_str(attribute.name)
336-
click.echo(
337-
f"{attribute.name:{_SHORT_NAME_WIDTH}}{attribute.constant:{_LONG_NAME_WIDTH}} = {value}"
338-
)
368+
if json_:
369+
if attribute.type_ == datetime.datetime:
370+
# need to convert datetime.datetime to string to serialize
371+
value = md.get_attribute(attribute.name)
372+
if type(value) == list:
373+
value = [v.isoformat() for v in value]
374+
else:
375+
value = value.isoformat()
376+
data[attribute.constant] = value
377+
else:
378+
# get raw value
379+
data[attribute.constant] = md.get_attribute(attribute.name)
380+
else:
381+
value = md.get_attribute_str(attribute.name)
382+
click.echo(
383+
f"{attribute.name:{_SHORT_NAME_WIDTH}}{attribute.constant:{_LONG_NAME_WIDTH}} = {value}"
384+
)
339385
except KeyError:
340386
click.echo(
341387
f"{'UNKNOWN':{_SHORT_NAME_WIDTH}}{attr:{_LONG_NAME_WIDTH}} = THIS ATTRIBUTE NOT HANDLED"
342388
)
343-
344-
345-
# def write_json_data(fp, data):
346-
# json.dump(data, fp)
347-
# fp.write("\n")
348-
349-
350-
# def write_text_data(fp, data):
351-
# file = data["file"]
352-
353-
# fc = data["fc"]
354-
# fc = fc if fc is not None else ""
355-
356-
# dldate = data["dldate"]
357-
# dldate = dldate if dldate is not None else ""
358-
359-
# desc = data["description"]
360-
# desc = desc if desc is not None else ""
361-
362-
# where_from = data["where_from"]
363-
# where_from = where_from if where_from is not None else ""
364-
365-
# tags = data["tags"]
366-
# tags = tags if len(tags) != 0 else ""
367-
368-
# print(f"file: {file}", file=fp)
369-
# print(f"description: {desc}", file=fp)
370-
# print(f"tags: {tags}", file=fp)
371-
# print(f"Finder comment: {fc}", file=fp)
372-
# print(f"Download date: {dldate}", file=fp)
373-
# print(f"Where from: {where_from}", file=fp)
374-
# print("\n", file=fp)
389+
if json_:
390+
json_str = json.dumps(data)
391+
click.echo(json_str)
375392

376393

377394
# def restore_from_json(json_file, quiet=False):

osxmetadata/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.98.4"
1+
__version__ = "0.98.5"

tests/test_cli.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,62 @@ def test_datetime_list_attributes(temp_file, attribute):
206206
assert meta.get_attribute(attribute) == [dt]
207207

208208

209+
def test_get_json(temp_file):
210+
import json
211+
import pathlib
212+
from osxmetadata import OSXMetaData, ATTRIBUTES, __version__
213+
from osxmetadata.__main__ import cli
214+
215+
runner = CliRunner()
216+
result = runner.invoke(
217+
cli, ["--set", "tags", "foo", "--set", "tags", "bar", temp_file]
218+
)
219+
result = runner.invoke(cli, ["--get", "tags", "--json", temp_file])
220+
assert result.exit_code == 0
221+
json_ = json.loads(result.stdout)
222+
assert json_["com.apple.metadata:_kMDItemUserTags"] == ["foo", "bar"]
223+
assert json_["_version"] == __version__
224+
assert json_["_filename"] == pathlib.Path(temp_file).name
225+
226+
227+
def test_list_json(temp_file):
228+
import json
229+
import pathlib
230+
from osxmetadata import OSXMetaData, ATTRIBUTES, __version__
231+
from osxmetadata.__main__ import cli
232+
233+
runner = CliRunner()
234+
result = runner.invoke(
235+
cli, ["--set", "tags", "foo", "--set", "tags", "bar", temp_file]
236+
)
237+
result = runner.invoke(cli, ["--list", "--json", temp_file])
238+
assert result.exit_code == 0
239+
json_ = json.loads(result.stdout)
240+
assert json_["com.apple.metadata:_kMDItemUserTags"] == ["foo", "bar"]
241+
assert json_["_version"] == __version__
242+
assert json_["_filename"] == pathlib.Path(temp_file).name
243+
244+
245+
def test_cli_error_json(temp_file):
246+
from osxmetadata import OSXMetaData, ATTRIBUTES
247+
from osxmetadata.__main__ import cli
248+
249+
runner = CliRunner()
250+
result = runner.invoke(cli, ["--set", "tags", "foo", "--json", temp_file])
251+
assert result.exit_code == 2
252+
assert "--json can only be used with --get or --list" in result.stdout
253+
254+
255+
def test_cli_error_bad_attribute(temp_file):
256+
from osxmetadata import OSXMetaData, ATTRIBUTES
257+
from osxmetadata.__main__ import cli
258+
259+
runner = CliRunner()
260+
result = runner.invoke(cli, ["--set", "foo", "bar", temp_file])
261+
assert result.exit_code == 2
262+
assert "Invalid attribute foo" in result.stdout
263+
264+
209265
def test_cli_error(temp_file):
210266
from osxmetadata.__main__ import cli
211267

0 commit comments

Comments
 (0)