# Path: egtlib/meta.py
# class Meta:
#     """
#     Metadata about a project.
# 
#     This is the first section of the project file, and can be omitted.
#     """
#     re_meta_head = re.compile(r"^\w.*:")
# 
#     def __init__(self):
#         # Line number in the project file where the metadata start
#         self._lineno: Optional[int] = None
# 
#         # Dict mapping lowercase field names to their string values
#         self._raw = OrderedDict()
# 
#         # Set of tags for the project
#         self.tags = set()
# 
#     def copy(self):
#         """
#         Return a copy of this metadata
#         """
#         res = Meta()
#         res._raw = self._raw.copy()
#         res.tags = self.tags.copy()
#         return res
# 
#     def has(self, name: str) -> bool:
#         """
#         Check if the given field is set in the metadata
#         """
#         return name.lower() in self._raw
# 
#     def get(self, name: str, *args) -> str:
#         """
#         Get a metadata element by name, optionally with a default.
#         """
#         name = name.lower()
#         if hasattr(self, name):
#             return getattr(self, name)
#         return self._raw.get(name, *args)
# 
#     def set(self, name: str, value: str) -> None:
#         """
#         Set the value of a metadata element
#         """
#         self._raw[name.lower()] = value
# 
#     def unset(self, name: str) -> None:
#         """
#         Unset the value of a metadata element, if it exists, otherwise does
#         nothing
#         """
#         self._raw.pop(name.lower(), None)
# 
#     def set_durations(self, durations: Dict[str, int]) -> None:
#         """
#         Set the Total: header from the given computed durations
#         """
#         if len(durations) == 1:
#             self.set("total", format_duration(durations[""]))
#         else:
#             lines = []
#             for tag, duration in sorted(durations.items()):
#                 if not tag:
#                     tag = "*"
#                 lines.append("{}: {}".format(tag, format_duration(duration)))
#             self.set("total", "\n".join(lines))
# 
#     def parse(self, lines: Lines) -> None:
#         """
#         Parse a metadata section from a Lines object
#         """
#         # Parse raw lines
#         self._lineno = lines.lineno
# 
#         # Get everything until we reach an empty line
#         meta_lines: List[str] = []
#         while True:
#             # Stop at an empty line or at EOF
#             if not lines.peek():
#                 lines.skip_empty_lines()
#                 break
#             meta_lines.append(lines.next())
# 
#         # Parse fields in the same way as email headers
#         import email
#         for k, v in email.message_from_string("\n".join(meta_lines)).items():
#             if v is None:
#                 continue
#             self._raw[k.lower()] = inspect.cleandoc(str(v))
# 
#         # Extract well known values
# 
#         # Tags
#         f = self._raw.get("tags", None)
#         if f is not None:
#             self.tags.update(re.split(r"[ ,\t]+", f))
# 
#     def print(self, file: TextIO = sys.stdout) -> bool:
#         """
#         Write the metadata as a project metadata section to the given output
#         file.
# 
#         Returns True if the metadata section was printed, False if there was
#         nothing to print.
#         """
#         res = False
#         for name, value in self._raw.items():
#             res = True
#             if "\n" in value:
#                 print("{}:".format(name.title()), file=file)
#                 for line in value.splitlines():
#                     print(" " + line, file=file)
#             else:
#                 print("{}: {}".format(name.title(), value.strip()), file=file)
#         return res
# 
#     @classmethod
#     def is_start_line(cls, line: str) -> bool:
#         return bool(cls.re_meta_head.match(line))
# 
# Path: egtlib/parse.py
# class Lines:
#     """
#     Access a text file line by line, with line numbers
#     """
#     def __init__(self, pathname: str, fd: Optional[TextIO] = None):
#         # File name being parsed
#         self.fname = pathname
#         # Current line being parsed
#         self.lineno = 0
#         # Read the file, split in trimmed lines
#         self.lines: List[str]
#         if fd is None:
#             with open(self.fname, "rt") as fd:
#                 self.lines = [x.rstrip() for x in fd]
#         else:
#             self.lines = [x.rstrip() for x in fd]
# 
#     def peek(self) -> Optional[str]:
#         """
#         Return the next line to be parsed, without advancing the cursor.
#         Return None if we are at the end.
#         """
#         if self.lineno < len(self.lines):
#             return self.lines[self.lineno]
#         else:
#             return None
# 
#     def next(self) -> str:
#         """
#         Return the next line to be parsed, advancing the cursor.
#         Raise RuntimeError if we are at the end.
#         """
#         if self.lineno >= len(self.lines):
#             raise RuntimeError("Lines.next() called at the end of the input")
#         res = self.lines[self.lineno]
#         self.lineno += 1
#         return res
# 
#     def rest(self) -> Generator[str, None, None]:
#         """
#         Generate all remaining lines
#         """
#         yield from self.lines[self.lineno:]
# 
#     def discard(self) -> None:
#         """
#         Just advance the cursor to the next line
#         """
#         if self.lineno < len(self.lines):
#             self.lineno += 1
# 
#     def skip_empty_lines(self) -> None:
#         while True:
#             line = self.peek()
#             if line is None:
#                 break
#             if line:
#                 break
#             self.discard()


import unittest
import io
from egtlib.meta import Meta
from egtlib.parse import Lines

TEST_META1 = (
    "Field: value\n"
    "lowercase: value1  \n"
    "UPPERCASE:  value2 \n"
    "multiline: \n"
    "  foobar\n"
    "   baz\n"
    "tags:  a,  b,  c \n"
    "\n"
    "2019\n"

# Path: egtlib/meta.py
# class Meta:
#     """
#     Metadata about a project.
# 
#     This is the first section of the project file, and can be omitted.
#     """
#     re_meta_head = re.compile(r"^\w.*:")
# 
#     def __init__(self):
#         # Line number in the project file where the metadata start
#         self._lineno: Optional[int] = None
# 
#         # Dict mapping lowercase field names to their string values
#         self._raw = OrderedDict()
# 
#         # Set of tags for the project
#         self.tags = set()
# 
#     def copy(self):
#         """
#         Return a copy of this metadata
#         """
#         res = Meta()
#         res._raw = self._raw.copy()
#         res.tags = self.tags.copy()
#         return res
# 
#     def has(self, name: str) -> bool:
#         """
#         Check if the given field is set in the metadata
#         """
#         return name.lower() in self._raw
# 
#     def get(self, name: str, *args) -> str:
#         """
#         Get a metadata element by name, optionally with a default.
#         """
#         name = name.lower()
#         if hasattr(self, name):
#             return getattr(self, name)
#         return self._raw.get(name, *args)
# 
#     def set(self, name: str, value: str) -> None:
#         """
#         Set the value of a metadata element
#         """
#         self._raw[name.lower()] = value
# 
#     def unset(self, name: str) -> None:
#         """
#         Unset the value of a metadata element, if it exists, otherwise does
#         nothing
#         """
#         self._raw.pop(name.lower(), None)
# 
#     def set_durations(self, durations: Dict[str, int]) -> None:
#         """
#         Set the Total: header from the given computed durations
#         """
#         if len(durations) == 1:
#             self.set("total", format_duration(durations[""]))
#         else:
#             lines = []
#             for tag, duration in sorted(durations.items()):
#                 if not tag:
#                     tag = "*"
#                 lines.append("{}: {}".format(tag, format_duration(duration)))
#             self.set("total", "\n".join(lines))
# 
#     def parse(self, lines: Lines) -> None:
#         """
#         Parse a metadata section from a Lines object
#         """
#         # Parse raw lines
#         self._lineno = lines.lineno
# 
#         # Get everything until we reach an empty line
#         meta_lines: List[str] = []
#         while True:
#             # Stop at an empty line or at EOF
#             if not lines.peek():
#                 lines.skip_empty_lines()
#                 break
#             meta_lines.append(lines.next())
# 
#         # Parse fields in the same way as email headers
#         import email
#         for k, v in email.message_from_string("\n".join(meta_lines)).items():
#             if v is None:
#                 continue
#             self._raw[k.lower()] = inspect.cleandoc(str(v))
# 
#         # Extract well known values
# 
#         # Tags
#         f = self._raw.get("tags", None)
#         if f is not None:
#             self.tags.update(re.split(r"[ ,\t]+", f))
# 
#     def print(self, file: TextIO = sys.stdout) -> bool:
#         """
#         Write the metadata as a project metadata section to the given output
#         file.
# 
#         Returns True if the metadata section was printed, False if there was
#         nothing to print.
#         """
#         res = False
#         for name, value in self._raw.items():
#             res = True
#             if "\n" in value:
#                 print("{}:".format(name.title()), file=file)
#                 for line in value.splitlines():
#                     print(" " + line, file=file)
#             else:
#                 print("{}: {}".format(name.title(), value.strip()), file=file)
#         return res
# 
#     @classmethod
#     def is_start_line(cls, line: str) -> bool:
#         return bool(cls.re_meta_head.match(line))
# 
# Path: egtlib/parse.py
# class Lines:
#     """
#     Access a text file line by line, with line numbers
#     """
#     def __init__(self, pathname: str, fd: Optional[TextIO] = None):
#         # File name being parsed
#         self.fname = pathname
#         # Current line being parsed
#         self.lineno = 0
#         # Read the file, split in trimmed lines
#         self.lines: List[str]
#         if fd is None:
#             with open(self.fname, "rt") as fd:
#                 self.lines = [x.rstrip() for x in fd]
#         else:
#             self.lines = [x.rstrip() for x in fd]
# 
#     def peek(self) -> Optional[str]:
#         """
#         Return the next line to be parsed, without advancing the cursor.
#         Return None if we are at the end.
#         """
#         if self.lineno < len(self.lines):
#             return self.lines[self.lineno]
#         else:
#             return None
# 
#     def next(self) -> str:
#         """
#         Return the next line to be parsed, advancing the cursor.
#         Raise RuntimeError if we are at the end.
#         """
#         if self.lineno >= len(self.lines):
#             raise RuntimeError("Lines.next() called at the end of the input")
#         res = self.lines[self.lineno]
#         self.lineno += 1
#         return res
# 
#     def rest(self) -> Generator[str, None, None]:
#         """
#         Generate all remaining lines
#         """
#         yield from self.lines[self.lineno:]
# 
#     def discard(self) -> None:
#         """
#         Just advance the cursor to the next line
#         """
#         if self.lineno < len(self.lines):
#             self.lineno += 1
# 
#     def skip_empty_lines(self) -> None:
#         while True:
#             line = self.peek()
#             if line is None:
#                 break
#             if line:
#                 break
#             self.discard()


# Path: test/test_meta.py
import unittest
import io
from egtlib.meta import Meta
from egtlib.parse import Lines


TEST_META1 = (
    "Field: value\n"
    "lowercase: value1  \n"
    "UPPERCASE:  value2 \n"
    "multiline: \n"
    "  foobar\n"
    "   baz\n"
    "tags:  a,  b,  c \n"
    "\n"
    "2019\n"

)