orgmunge 0.2.0__tar.gz → 0.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,19 @@
1
+ Metadata-Version: 2.1
2
+ Name: orgmunge
3
+ Version: 0.2.2
4
+ Summary: Programmatically modify Orgmode documents
5
+ License: MIT
6
+ Author: durableOne
7
+ Author-email: era31asj@gmail.com
8
+ Requires-Python: >=3.10,<4.0
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Requires-Dist: ply (>=3.11,<4.0)
15
+ Description-Content-Type: text/plain
16
+
1
17
  #+title:Orgmunge
2
18
  * Motivation and scope
3
19
  Orgmunge was born out of the desire to modify Org documents
@@ -120,21 +136,25 @@ compromising your information. See [[https://github.com/durableOne/redactOrg][re
120
136
  values. The properties are parsed from the =PROPERTIES= drawer if
121
137
  it exists. This attribute can also be set by the user (the value
122
138
  supplied must be a dict).
123
- 2. =headline= returns the heading's headline. This attribute can also
139
+ 2. =inherited_properties=. Same format as the =properties= dict but
140
+ contains only properties inherited from ancestors.
141
+ 3. =tags= returns a list of all tags (those explicitly defined for
142
+ this heading and those inherited)
143
+ 4. =headline= returns the heading's headline. This attribute can also
124
144
  be set by a user (the value must be a [[*Headline Objects][Headline]] instance).
125
- 3. =scheduling= is a [[*Scheduling Objects][Scheduling]] object containing information about
145
+ 5. =scheduling= is a [[*Scheduling Objects][Scheduling]] object containing information about
126
146
  =SCHEDULED/DEADLINE/CLOSED= timestamps of the heading, if any. Can
127
147
  also be set by the user (the value must be a Scheduling instance).
128
- 4. =drawers= is a list of [[*Drawer Objects][Drawer]] objects containing the drawers
148
+ 6. =drawers= is a list of [[*Drawer Objects][Drawer]] objects containing the drawers
129
149
  associated with this heading. When you update the heading's
130
150
  =properties= attribute, the =PROPERTIES= drawer is updated the next
131
151
  time you access it.
132
- 5. =children= returns a list of Heading objects that are the direct
152
+ 7. =children= returns a list of Heading objects that are the direct
133
153
  children of this heading.
134
- 6. =parent= returns the parent heading of the current one. If the
154
+ 8. =parent= returns the parent heading of the current one. If the
135
155
  current heading is a top-level heading, the root heading will be
136
156
  returned.
137
- 7. =sibling= returns the sibling heading of the current one that comes
157
+ 9. =sibling= returns the sibling heading of the current one that comes
138
158
  before it in the tree, if any. The reason this is the sibling
139
159
  heading that is formally tracked is because it's the one that
140
160
  would adopt the current heading whenever the current heading is
@@ -143,34 +163,37 @@ compromising your information. See [[https://github.com/durableOne/redactOrg][re
143
163
  #+begin_src python
144
164
  siblings = [c for c in current_heading.parent.children if c is not current_heading]
145
165
  #+end_src
146
- 8. =level= is the heading's level, with 1 being the top level and each
147
- sub-level after that being incremented by 1 (the heading's level
148
- is the number of "stars" before its headline).
166
+ 10. =level= is the heading's level, with 1 being the top level and each
167
+ sub-level after that being incremented by 1 (the heading's level
168
+ is the number of "stars" before its headline).
149
169
  - Important methods:
150
170
  1. =clocking=. This returns a list of [[*Clocking Objects][Clocking]] objects, parsed
151
171
  from the heading's =LOGBOOK= drawer, if any. You can also pass the
152
172
  optional boolean parameter =include_children=, which, when True,
153
173
  includes clocking information of this heading's children as well.
154
- 2. =add_child= accepts a Heading object to add as a child to the
174
+ 2. =get_all_properties=. This returns a dict of all properties of the
175
+ heading, whether directly defined or inherited from the heading's
176
+ ancestors. The latest-defined value of a property wins over.
177
+ 3. =add_child= accepts a Heading object to add as a child to the
155
178
  current heading. The optional boolean parameter =new= should be set
156
179
  to =True= when this is a new heading that was created and needs to
157
180
  be assigned a parent. It should be set to =False= (default) when
158
181
  the addition of a child is due to a promotion/demotion operation.
159
- 3. =remove_child= accepts a heading object and deletes it from the
182
+ 4. =remove_child= accepts a heading object and deletes it from the
160
183
  current heading's children if it's a child of the current
161
184
  heading.
162
- 4. =promote= promotes the current heading one level. If the heading has
185
+ 5. =promote= promotes the current heading one level. If the heading has
163
186
  children, they would be orphaned so this raises a
164
187
  =ValueError=. Technically, Org allows you to have, say, level 3
165
188
  headings under a level 1 heading, but =orgmunge= does not allow
166
189
  this to make parsing the tree easier.
167
- 5. =promote_tree= promotes the current heading and all its
190
+ 6. =promote_tree= promotes the current heading and all its
168
191
  descendants. Use this if the heading you want to promote has
169
192
  children.
170
- 6. =demote= demotes the current heading one level. If the current
193
+ 7. =demote= demotes the current heading one level. If the current
171
194
  heading has no sibling to adopt it, the demotion attempt fails
172
195
  and raises a =ValueError=.
173
- 7. =demote_tree= is the equivalent of =promote_tree= for demotion.
196
+ 8. =demote_tree= is the equivalent of =promote_tree= for demotion.
174
197
  *** Headline Objects
175
198
  - Important attributes:
176
199
  1. =done= is a boolean attribute that determines whether the headline
@@ -295,6 +318,16 @@ ignored if =exact= is =True=.
295
318
  Uses =filter_headings= under the hood so will return a generator of
296
319
  matching headings.
297
320
 
321
+ *** Search for Headings by Path
322
+ Use =Org.get_heading_by_path= to search for a heading with the given path:
323
+ #+begin_src python
324
+ Org.get_heading_by_path(path, exact=False, re_flags=0)
325
+ #+end_src
326
+ =path= is a list of heading titles. Each member is interpreted the same
327
+ way the =search_string= argument of =get_headings_by_title= is
328
+ interpreted. This function returns the first heading of the tree that
329
+ matches the given path or =None= if no such heading is found.
330
+
298
331
  * License
299
332
  #+INCLUDE: ./LICENSE
300
333
 
@@ -312,3 +345,4 @@ Thanks to these wonderful people for contributing time and code:
312
345
  - [[https://github.com/Nalisarc][Nalisarc]]
313
346
  - [[https://github.com/ispringle][ispringle]]
314
347
  - [[https://github.com/crdoconnor][crdoconnor]]
348
+
@@ -1,18 +1,3 @@
1
- Metadata-Version: 2.1
2
- Name: orgmunge
3
- Version: 0.2.0
4
- Summary: Programmatically modify Orgmode documents
5
- License: MIT
6
- Author: durableOne
7
- Author-email: era31asj@gmail.com
8
- Requires-Python: >=3.10,<4.0
9
- Classifier: License :: OSI Approved :: MIT License
10
- Classifier: Programming Language :: Python :: 3
11
- Classifier: Programming Language :: Python :: 3.10
12
- Classifier: Programming Language :: Python :: 3.11
13
- Requires-Dist: ply (>=3.11,<4.0)
14
- Description-Content-Type: text/plain
15
-
16
1
  #+title:Orgmunge
17
2
  * Motivation and scope
18
3
  Orgmunge was born out of the desire to modify Org documents
@@ -135,21 +120,25 @@ compromising your information. See [[https://github.com/durableOne/redactOrg][re
135
120
  values. The properties are parsed from the =PROPERTIES= drawer if
136
121
  it exists. This attribute can also be set by the user (the value
137
122
  supplied must be a dict).
138
- 2. =headline= returns the heading's headline. This attribute can also
123
+ 2. =inherited_properties=. Same format as the =properties= dict but
124
+ contains only properties inherited from ancestors.
125
+ 3. =tags= returns a list of all tags (those explicitly defined for
126
+ this heading and those inherited)
127
+ 4. =headline= returns the heading's headline. This attribute can also
139
128
  be set by a user (the value must be a [[*Headline Objects][Headline]] instance).
140
- 3. =scheduling= is a [[*Scheduling Objects][Scheduling]] object containing information about
129
+ 5. =scheduling= is a [[*Scheduling Objects][Scheduling]] object containing information about
141
130
  =SCHEDULED/DEADLINE/CLOSED= timestamps of the heading, if any. Can
142
131
  also be set by the user (the value must be a Scheduling instance).
143
- 4. =drawers= is a list of [[*Drawer Objects][Drawer]] objects containing the drawers
132
+ 6. =drawers= is a list of [[*Drawer Objects][Drawer]] objects containing the drawers
144
133
  associated with this heading. When you update the heading's
145
134
  =properties= attribute, the =PROPERTIES= drawer is updated the next
146
135
  time you access it.
147
- 5. =children= returns a list of Heading objects that are the direct
136
+ 7. =children= returns a list of Heading objects that are the direct
148
137
  children of this heading.
149
- 6. =parent= returns the parent heading of the current one. If the
138
+ 8. =parent= returns the parent heading of the current one. If the
150
139
  current heading is a top-level heading, the root heading will be
151
140
  returned.
152
- 7. =sibling= returns the sibling heading of the current one that comes
141
+ 9. =sibling= returns the sibling heading of the current one that comes
153
142
  before it in the tree, if any. The reason this is the sibling
154
143
  heading that is formally tracked is because it's the one that
155
144
  would adopt the current heading whenever the current heading is
@@ -158,34 +147,37 @@ compromising your information. See [[https://github.com/durableOne/redactOrg][re
158
147
  #+begin_src python
159
148
  siblings = [c for c in current_heading.parent.children if c is not current_heading]
160
149
  #+end_src
161
- 8. =level= is the heading's level, with 1 being the top level and each
162
- sub-level after that being incremented by 1 (the heading's level
163
- is the number of "stars" before its headline).
150
+ 10. =level= is the heading's level, with 1 being the top level and each
151
+ sub-level after that being incremented by 1 (the heading's level
152
+ is the number of "stars" before its headline).
164
153
  - Important methods:
165
154
  1. =clocking=. This returns a list of [[*Clocking Objects][Clocking]] objects, parsed
166
155
  from the heading's =LOGBOOK= drawer, if any. You can also pass the
167
156
  optional boolean parameter =include_children=, which, when True,
168
157
  includes clocking information of this heading's children as well.
169
- 2. =add_child= accepts a Heading object to add as a child to the
158
+ 2. =get_all_properties=. This returns a dict of all properties of the
159
+ heading, whether directly defined or inherited from the heading's
160
+ ancestors. The latest-defined value of a property wins over.
161
+ 3. =add_child= accepts a Heading object to add as a child to the
170
162
  current heading. The optional boolean parameter =new= should be set
171
163
  to =True= when this is a new heading that was created and needs to
172
164
  be assigned a parent. It should be set to =False= (default) when
173
165
  the addition of a child is due to a promotion/demotion operation.
174
- 3. =remove_child= accepts a heading object and deletes it from the
166
+ 4. =remove_child= accepts a heading object and deletes it from the
175
167
  current heading's children if it's a child of the current
176
168
  heading.
177
- 4. =promote= promotes the current heading one level. If the heading has
169
+ 5. =promote= promotes the current heading one level. If the heading has
178
170
  children, they would be orphaned so this raises a
179
171
  =ValueError=. Technically, Org allows you to have, say, level 3
180
172
  headings under a level 1 heading, but =orgmunge= does not allow
181
173
  this to make parsing the tree easier.
182
- 5. =promote_tree= promotes the current heading and all its
174
+ 6. =promote_tree= promotes the current heading and all its
183
175
  descendants. Use this if the heading you want to promote has
184
176
  children.
185
- 6. =demote= demotes the current heading one level. If the current
177
+ 7. =demote= demotes the current heading one level. If the current
186
178
  heading has no sibling to adopt it, the demotion attempt fails
187
179
  and raises a =ValueError=.
188
- 7. =demote_tree= is the equivalent of =promote_tree= for demotion.
180
+ 8. =demote_tree= is the equivalent of =promote_tree= for demotion.
189
181
  *** Headline Objects
190
182
  - Important attributes:
191
183
  1. =done= is a boolean attribute that determines whether the headline
@@ -310,6 +302,16 @@ ignored if =exact= is =True=.
310
302
  Uses =filter_headings= under the hood so will return a generator of
311
303
  matching headings.
312
304
 
305
+ *** Search for Headings by Path
306
+ Use =Org.get_heading_by_path= to search for a heading with the given path:
307
+ #+begin_src python
308
+ Org.get_heading_by_path(path, exact=False, re_flags=0)
309
+ #+end_src
310
+ =path= is a list of heading titles. Each member is interpreted the same
311
+ way the =search_string= argument of =get_headings_by_title= is
312
+ interpreted. This function returns the first heading of the tree that
313
+ matches the given path or =None= if no such heading is found.
314
+
313
315
  * License
314
316
  #+INCLUDE: ./LICENSE
315
317
 
@@ -327,4 +329,3 @@ Thanks to these wonderful people for contributing time and code:
327
329
  - [[https://github.com/Nalisarc][Nalisarc]]
328
330
  - [[https://github.com/ispringle][ispringle]]
329
331
  - [[https://github.com/crdoconnor][crdoconnor]]
330
-
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "orgmunge"
3
- version = "0.2.0"
3
+ version = "0.2.2"
4
4
  description = "Programmatically modify Orgmode documents"
5
5
  authors = ["durableOne <era31asj@gmail.com>"]
6
6
  license = "MIT"
@@ -7,7 +7,7 @@ import platform
7
7
  from .parser import Parser
8
8
  from .lexer import Lexer
9
9
  from .classes import *
10
- from typing import List, Dict, Optional, Generator, Callable
10
+ from typing import List, Dict, Optional, Generator, Callable, Iterable
11
11
 
12
12
  class Org:
13
13
  def _parse_todos(self, inp: str) -> Optional[Dict[str, Dict[str, str]]]:
@@ -164,6 +164,25 @@ class Org:
164
164
  condition = lambda h: bool(re.search(fr'{search_string}', h.title, flags=re_flags))
165
165
  return self.filter_headings(condition)
166
166
 
167
+ def get_heading_by_path(self, path: List[str], exact: bool = False, re_flags: int = 0) -> Optional[Heading]:
168
+ """Return a heading by its given path. If exact is True, interpret the given path
169
+ as strings to match node titles exactly; otherwise, interpret them as regexes. If
170
+ no heading matches the given path, return None"""
171
+ if exact:
172
+ condition = lambda heading: heading.title == path[-1]
173
+ else:
174
+ condition = lambda heading: bool(re.search(fr'{path[-1]}', heading.title, flags=re_flags))
175
+ if not path:
176
+ return self.root
177
+ else:
178
+ headings_at_this_level = self.filter_headings(lambda h: h.level == len(path) and condition(h))
179
+ candidates = (h for h in headings_at_this_level
180
+ if h.parent is self.get_heading_by_path(path[:-1], exact=exact, re_flags=re_flags))
181
+ try:
182
+ return next(candidates)
183
+ except StopIteration:
184
+ return None
185
+
167
186
  def __repr__(self):
168
187
  result = ''
169
188
  for keyword in self.metadata:
@@ -135,7 +135,6 @@ class Priority:
135
135
  return False
136
136
  else:
137
137
  return str(self) == str(other)
138
-
139
138
  class Headline:
140
139
  def __init__(self, todos, level: str, comment: bool = False,
141
140
  todo: Optional[str] = None, priority: Optional[str] = None,
@@ -159,10 +158,10 @@ class Headline:
159
158
  raise AttributeError("Can't set the 'done' attribute")
160
159
 
161
160
  def _is_done(self):
162
- if self.todo in self._done_states:
163
- return True
164
- elif self.todo is None or self.todo in self._todo_states:
161
+ if self.todo is None or self.todo in self._todo_states:
165
162
  return False
163
+ elif self.todo in self._done_states:
164
+ return True
166
165
  else:
167
166
  raise ValueError(f"Uncategorized todo state {self.todo}")
168
167
 
@@ -252,7 +251,6 @@ class Headline:
252
251
  return False
253
252
  else:
254
253
  return str(self) == str(other)
255
-
256
254
  class TimeStamp:
257
255
  def __init__(self, timestamp_str: str):
258
256
  is_active = re.search(Lexer.ATIMESTAMP, timestamp_str)
@@ -376,7 +374,6 @@ class TimeStamp:
376
374
  return False
377
375
  else:
378
376
  return str(self) == str(other)
379
-
380
377
  class Scheduling:
381
378
  _closed = None
382
379
  _scheduled = None
@@ -442,7 +439,6 @@ class Scheduling:
442
439
  return False
443
440
  else:
444
441
  return str(self) == str(other)
445
-
446
442
  class Drawer:
447
443
  def __init__(self, drawer_string: str):
448
444
  self.name = re.sub(r':', '', drawer_string.split('\n')[0])
@@ -458,7 +454,6 @@ class Drawer:
458
454
  return False
459
455
  else:
460
456
  return str(self) == str(other)
461
-
462
457
  class Clocking:
463
458
  def __init__(self, start_time: str, end_time: Optional[str] = None):
464
459
  self._start_time = dt.strptime(start_time, ORG_TIME_FORMAT)
@@ -526,7 +521,6 @@ class Clocking:
526
521
  return False
527
522
  else:
528
523
  return str(self) == str(other)
529
-
530
524
  class Heading():
531
525
  def __init__(self, headline: Headline, contents: Tuple[Scheduling, List[Drawer], str]):
532
526
  self._headline = headline
@@ -534,6 +528,7 @@ class Heading():
534
528
  self._children = []
535
529
  self._parent = None
536
530
  self._sibling = None
531
+ self._inherited_properties = dict()
537
532
  if self.body:
538
533
  self.timestamps = [TimeStamp(t[0]) for t in re.findall(fr'({Lexer.ATIMESTAMP}|{Lexer.ITIMESTAMP})', self.body)]
539
534
  if self._drawers:
@@ -567,9 +562,9 @@ class Heading():
567
562
  def _get_clocking_info(self) -> List[Clocking]:
568
563
  if not self.drawers:
569
564
  return []
570
- logbook = [d for d in self.drawers if d.name == 'LOGBOOK']
565
+ logbook = self.get_drawer_by_name('LOGBOOK')
571
566
  if logbook:
572
- return [self._parse_clock_line(l) for l in logbook[0].contents if re.search(r'^\s*CLOCK:', l)]
567
+ return [self._parse_clock_line(l) for l in logbook.contents if re.search(r'^\s*CLOCK:', l)]
573
568
  else:
574
569
  return []
575
570
 
@@ -580,6 +575,27 @@ class Heading():
580
575
  def _get_properties_string(self) -> str:
581
576
  return "\n".join([f":{k}:{' '*7}{v}" for k, v in self.properties.items()])
582
577
 
578
+ @property
579
+ def inherited_properties(self):
580
+ if not self._inherited_properties:
581
+ if self.parent:
582
+ self._inherited_properties = {**self.parent.inherited_properties, **self.parent.properties}
583
+ return self._inherited_properties
584
+
585
+ @inherited_properties.setter
586
+ def inherited_properties(self, _):
587
+ raise AttributeError("Can't set the inherited properties of a heading")
588
+
589
+ @property
590
+ def tags(self):
591
+ self_tags = self.headline.tags or []
592
+ parent_tags = (self.parent.tags if self.parent else None) or []
593
+ return list(set(self_tags + parent_tags))
594
+
595
+ @tags.setter
596
+ def tags(self, _):
597
+ raise AttributeError("Can't set the tags of a heading, use headline instead.")
598
+
583
599
  @property
584
600
  def properties(self):
585
601
  return self._properties
@@ -593,6 +609,8 @@ class Heading():
593
609
  for key in val:
594
610
  self._properties[key] = val[key]
595
611
 
612
+ def get_all_properties(self) -> Dict[str, str]:
613
+ return {**self.inherited_properties, **self.properties}
596
614
 
597
615
  def clocking(self, include_children: bool = False) -> List[Clocking]:
598
616
  "Return the clocking information of the given headline and possibly its children."
@@ -602,6 +620,13 @@ class Heading():
602
620
  else:
603
621
  return own_clocking
604
622
 
623
+ def get_drawer_by_name(self, name: str) -> Optional[Drawer]:
624
+ "Return the named drawer if it exists, or None if it doesn't"
625
+ try:
626
+ return next(d for d in self.drawers if d.name == name)
627
+ except StopIteration:
628
+ return None
629
+
605
630
  @property
606
631
  def headline(self):
607
632
  return self._headline
@@ -34,7 +34,7 @@ class Lexer:
34
34
 
35
35
  def t_error(t):
36
36
  print(f"Illegal character encountered: {t.value[0]}")
37
- t.lexer.skip(1)
37
+ raise ValueError("Lexer error!")
38
38
 
39
39
  self.todos = todos
40
40
  all_todo_keywords = {**todos['todo_states'], **todos['done_states']}
@@ -94,7 +94,7 @@ class Lexer:
94
94
  return t
95
95
 
96
96
  def t_SPACE(t):
97
- r'[ \t]+'
97
+ r'[^\S\r\n]+'
98
98
  return t
99
99
 
100
100
  def t_TEXT(t):