improve code quality add unittest

refactoring of for loops by introducing Walker class
complete type checks
This commit is contained in:
cclecle
2023-03-31 23:12:17 +01:00
parent 46eab6f7b0
commit 66774be574
10 changed files with 232 additions and 121 deletions

View File

@@ -1,6 +1,8 @@
eclipse.preferences.version=1
encoding//src/pysimpleini/document.py=utf-8
encoding//src/pysimpleini/exceptions.py=utf-8
encoding//src/pysimpleini/keys.py=utf-8
encoding//src/pysimpleini/sections.py=utf-8
encoding//src/pysimpleini/simpleini.py=utf-8
encoding//src/pysimpleini/tools.py=utf-8
encoding/<project>=UTF-8

View File

@@ -28,8 +28,11 @@ from .exceptions import (
PySimpleINI_WrongFormatException,
)
from .keys import PySimpleINI_key_Base, PySimpleINI_key_Blank, PySimpleINI_key_Comment, PySimpleINI_key
from .simpleini import PySimpleINI
from .sections import (
PySimpleINI_Section_Base,
PySimpleINI_Section_Blank,
@@ -37,5 +40,3 @@ from .sections import (
PySimpleINI_Section,
PySimpleINI_Section_Root,
)
from .simpleini import PySimpleINI

View File

@@ -45,13 +45,17 @@ class PySimpleINI_Document:
@property
def sections(self):
"""sections getter"""
return self._sections
def reset_sections(self):
"""reset sections list"""
self._sections = []
class PySimpleINI_Document_Parser(PySimpleINI_Document):
"""Document Parser class"""
def __init__(self, bStrict: bool = False) -> None:
super().__init__()
self._lastsection: Optional[PySimpleINI_Section] = None
@@ -136,6 +140,8 @@ class PySimpleINI_Document_Parser(PySimpleINI_Document):
class PySimpleINI_Document_Formater(PySimpleINI_Document):
"""Document formater class"""
def format(self, bBeautify: bool = False, bWipeComments: bool = False) -> str:
"""Generate the full formated output.

View File

@@ -31,7 +31,6 @@ class PySimpleINI_key_Base(metaclass=ABCMeta):
Returns:
requested value
"""
pass
def format(self) -> str:
"""Key value formater (renderer)

View File

@@ -17,8 +17,9 @@ from typing import TYPE_CHECKING
from abc import ABCMeta, abstractmethod
from .keys import PySimpleINI_key_Base, PySimpleINI_key_Blank, PySimpleINI_key_Comment, PySimpleINI_key
from .exceptions import PySimpleINI_KeyNotFoundException
from .tools import Walker
from .keys import PySimpleINI_key_Base, PySimpleINI_key_Blank, PySimpleINI_key_Comment, PySimpleINI_key
if TYPE_CHECKING: # pragma: no cover # Only imports the below statements during type checking
from typing import List, Optional
@@ -199,28 +200,10 @@ class PySimpleINI_Section(PySimpleINI_Section_Comment, PySimpleINI_Section_Blank
Returns:
Number of deleted keys
"""
keys = self.getkey(name)
if isinstance(keys, PySimpleINI_key):
keys = [keys]
i = 0
ndeleted: int = 0
for key in keys:
if isinstance(key, PySimpleINI_key):
indexok = True
if index is not None:
indexok = False
if index == i:
indexok = True
valueok = True
if value is not None:
valueok = False
if value == key.getvalue():
valueok = True
if indexok and valueok:
self.keys.remove(key)
ndeleted = ndeleted + 1
i = i + 1
walker = Walker[PySimpleINI_key_Base](targetname=name, targetindex=index, targetvalue=value, targettypes=PySimpleINI_key)
walker.walk(self.keys, self.keys.remove)
ndeleted: int = walker.num_matched
if ndeleted == 0:
raise PySimpleINI_KeyNotFoundException()
@@ -250,22 +233,8 @@ class PySimpleINI_Section(PySimpleINI_Section_Comment, PySimpleINI_Section_Blank
result: List[PySimpleINI_key] = []
i = 0
for key in self.keys:
if isinstance(key, PySimpleINI_key) and (name == key.getname()):
indexok = True
if index is not None:
indexok = False
if index == i:
indexok = True
valueok = True
if value is not None:
valueok = False
if value == key.getvalue():
valueok = True
if indexok and valueok:
result.append(key)
i = i + 1
walker = Walker[PySimpleINI_key_Base](targetname=name, targetindex=index, targetvalue=value, targettypes=PySimpleINI_key)
walker.walk(self.keys, result.append)
if len(result) == 0:
raise PySimpleINI_KeyNotFoundException()

View File

@@ -1,6 +1,5 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# PySimpleINI (c) by chacha
#
# PySimpleINI is licensed under a
@@ -22,7 +21,8 @@ from typing import TYPE_CHECKING
from pathlib import Path
from .document import PySimpleINI_Document_Parser, PySimpleINI_Document_Formater
from .sections import PySimpleINI_Section, PySimpleINI_Section_Comment, PySimpleINI_Section_Blank
from .tools import Walker
from .sections import PySimpleINI_Section, PySimpleINI_Section_Comment, PySimpleINI_Section_Blank, PySimpleINI_Section_Base
from .keys import PySimpleINI_key
from .exceptions import PySimpleINI_SectionNotFoundException, PySimpleINI_KeyNotFoundException
@@ -60,21 +60,6 @@ class PySimpleINI(PySimpleINI_Document_Parser, PySimpleINI_Document_Formater):
"""
return self._filepath
def parsefile(self, filepath: Optional[Path | str]) -> PySimpleINI:
"""Parse a file.
Arg:
filepath: path of the file to parse
Returns:
self for convinience
"""
self._filepath = None
if isinstance(filepath, (str, Path)):
self._filepath = Path(filepath)
with open(self._filepath, encoding="utf-8") as file:
self.parse(file.read())
return self
@filepath.setter
def filepath(self, filepath: Optional[Path | str]) -> None:
"""Change / Set the INI file path.
@@ -90,6 +75,21 @@ class PySimpleINI(PySimpleINI_Document_Parser, PySimpleINI_Document_Formater):
if isinstance(filepath, (str, Path)):
self._filepath = Path(filepath)
def parsefile(self, filepath: Optional[Path | str]) -> PySimpleINI:
"""Parse a file.
Arg:
filepath: path of the file to parse
Returns:
self for convinience
"""
self._filepath = None
if isinstance(filepath, (str, Path)):
self._filepath = Path(filepath)
with open(self._filepath, encoding="utf-8") as file:
self.parse(file.read())
return self
def delsection(self, name: str, index: Optional[int] = None) -> int:
"""Delete an existing Section.
@@ -105,24 +105,14 @@ class PySimpleINI(PySimpleINI_Document_Parser, PySimpleINI_Document_Formater):
Returns:
Number of deleted sections
"""
sections = self.getsection(name)
if isinstance(sections, PySimpleINI_Section):
_sections = []
_sections.append(sections)
sections = _sections
i = 0
ndeleted = 0
for section in sections:
if isinstance(section, PySimpleINI_Section):
indexok = True
if index is not None:
indexok = False
if index == i:
indexok = True
if indexok:
self.sections.remove(section)
ndeleted = ndeleted + 1
i = i + 1
walker = Walker[PySimpleINI_Section_Base](targetname=name, targetindex=index, targettypes=PySimpleINI_Section)
walker.walk(self.sections, self.sections.remove)
ndeleted: int = walker.num_matched
if ndeleted == 0:
raise PySimpleINI_SectionNotFoundException()
return ndeleted
def addsection(self, name: str) -> PySimpleINI_Section:
@@ -146,7 +136,7 @@ class PySimpleINI(PySimpleINI_Document_Parser, PySimpleINI_Document_Formater):
The new section
"""
section = PySimpleINI_Section_Comment(-1)
section.appendCommentKey(self.sectioncomment_tags[0], self.sectioncomment_tags[0], -1)
section.appendCommentKey(self.sectioncomment_tags[0], value, -1)
self.sections.append(section)
return section
@@ -179,9 +169,10 @@ class PySimpleINI(PySimpleINI_Document_Parser, PySimpleINI_Document_Formater):
List of section names.
"""
result = []
for section in self.sections:
if isinstance(section, PySimpleINI_Section):
result.append(section.getname())
walker = Walker[PySimpleINI_Section_Base](targettypes=PySimpleINI_Section)
walker.walk(self.sections, lambda x: result.append(x.getname()))
return result
def getsection(self, name: str) -> PySimpleINI_Section | list[PySimpleINI_Section]:
@@ -199,10 +190,8 @@ class PySimpleINI(PySimpleINI_Document_Parser, PySimpleINI_Document_Formater):
result: List[PySimpleINI_Section] = []
for section in self.sections:
if isinstance(section, PySimpleINI_Section):
if name == section.getname():
result.append(section)
walker = Walker[PySimpleINI_Section_Base](targetname=name, targettypes=PySimpleINI_Section)
walker.walk(self.sections, result.append)
if len(result) == 0:
raise PySimpleINI_SectionNotFoundException()
@@ -223,14 +212,11 @@ class PySimpleINI(PySimpleINI_Document_Parser, PySimpleINI_Document_Formater):
Returns:
list of Keys names
"""
sections = self.getsection(sectionName)
if isinstance(sections, PySimpleINI_Section):
return sections.getallkeynames()
result = []
for section in sections:
if isinstance(section, PySimpleINI_Section):
result.extend(section.getallkeynames())
walker = Walker[PySimpleINI_Section_Base](targetname=sectionName, targettypes=PySimpleINI_Section)
walker.walk(self.sections, lambda x: result.extend(x.getallkeynames()))
return result
def delkey(self, sectionName: str, keyName: str) -> int:
@@ -267,24 +253,28 @@ class PySimpleINI(PySimpleINI_Document_Parser, PySimpleINI_Document_Formater):
Returns:
Number of deleted keys
"""
sections = self.getsection(sectionName)
if isinstance(sections, PySimpleINI_Section):
_sections = []
_sections.append(sections)
sections = _sections
ndeleted = 0
for section in sections:
if isinstance(section, PySimpleINI_Section):
walkersection = Walker[PySimpleINI_Section_Base](targetname=sectionName, targettypes=PySimpleINI_Section)
class CallbackWalkerSection:
"""temporary class to wrap ndeleted"""
ndeleted: int = 0
@classmethod
def CallbackWalkerSection(cls, section):
"""Walker callback"""
try:
ndeleted = ndeleted + section.delkey(keyName, index, value)
cls.ndeleted = cls.ndeleted + section.delkey(keyName, index, value)
except PySimpleINI_KeyNotFoundException:
pass
if ndeleted == 0:
walkersection.walk(self.sections, CallbackWalkerSection.CallbackWalkerSection)
if CallbackWalkerSection.ndeleted == 0:
raise PySimpleINI_KeyNotFoundException()
return ndeleted
return CallbackWalkerSection.ndeleted
def getkey(self, sectionName: str, keyName: str) -> PySimpleINI_key | List[PySimpleINI_key]:
"""Get one or more Key from Section.
@@ -322,27 +312,35 @@ class PySimpleINI(PySimpleINI_Document_Parser, PySimpleINI_Document_Formater):
Returns:
Found Keys
"""
sections = self.getsection(sectionName)
if isinstance(sections, PySimpleINI_Section):
return sections.getkey_ex(keyName, index, value, self._bForceAlwaysOutputArrays)
result = []
for section in sections:
if isinstance(section, PySimpleINI_Section):
walkersection = Walker[PySimpleINI_Section_Base](targetname=sectionName, targettypes=PySimpleINI_Section)
class CallbackWalkerSection:
"""temporary class to wrap result"""
result: List[PySimpleINI_key] = []
bForceAlwaysOutputArrays: bool = self._bForceAlwaysOutputArrays
@classmethod
def CallbackWalkerSection(cls, section):
"""Walker callback"""
try:
keys = section.getkey_ex(keyName, index, value, self._bForceAlwaysOutputArrays)
keys = section.getkey_ex(keyName, index, value, cls.bForceAlwaysOutputArrays)
if isinstance(keys, PySimpleINI_key):
result.append(keys)
cls.result.append(keys)
else: # array
result.extend(keys)
cls.result.extend(keys)
except PySimpleINI_KeyNotFoundException:
pass
if len(result) == 0:
walkersection.walk(self.sections, CallbackWalkerSection.CallbackWalkerSection)
if len(CallbackWalkerSection.result) == 0:
raise PySimpleINI_KeyNotFoundException()
if len(result) == 1 and (not self._bForceAlwaysOutputArrays):
return result[0]
return result
if len(CallbackWalkerSection.result) == 1 and (not self._bForceAlwaysOutputArrays):
return CallbackWalkerSection.result[0]
return CallbackWalkerSection.result
def getkeyvalue(self, sectionName: str, keyName: str) -> str | list[str]:
"""Get one or more Key's values from Section.

123
src/pysimpleini/tools.py Normal file
View File

@@ -0,0 +1,123 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# PySimpleINI (c) by chacha
#
# PySimpleINI is licensed under a
# Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Unported License.
#
# You should have received a copy of the license along with this
# work. If not, see <https://creativecommons.org/licenses/by-nc-sa/4.0/>.
"""Tools module
"""
from __future__ import annotations
from typing import TYPE_CHECKING, TypeVar, Generic, Protocol, runtime_checkable
from abc import abstractmethod
if TYPE_CHECKING: # pragma: no cover # Only imports the below statements during type checking
from typing import Optional, Callable, List
@runtime_checkable
class ProtoGetVal(Protocol):
"""generic protocol definition"""
@abstractmethod
def getvalue(self) -> str:
"""generic protocol method"""
@runtime_checkable
class ProtoGetName(Protocol):
"""generic protocol definition"""
@abstractmethod
def getname(self) -> str:
"""generic protocol method"""
T_Elem = TypeVar("T_Elem")
class Walker(Generic[T_Elem]):
"""Generic walker class to search key or section in a list"""
def __init__(self, **kwargs) -> None:
self.targetname: Optional[str] = None
if "targetname" in kwargs:
self.targetname = kwargs["targetname"]
self.targetvalue: Optional[str] = None
if "targetvalue" in kwargs:
self.targetvalue = kwargs["targetvalue"]
self.targetindex: Optional[str] = None
if "targetindex" in kwargs:
self.targetindex = kwargs["targetindex"]
self.targettypes: List[type[T_Elem]] = []
if "targettypes" in kwargs:
if isinstance(kwargs["targettypes"], list):
self.targettypes = kwargs["targettypes"]
else:
self.targettypes = [kwargs["targettypes"]]
self.index: int = 0
self.num_matched = 0
def reset(self) -> None:
"""Reset walker class context"""
self.index = 0
self.num_matched = 0
def _checkname(self, elem: T_Elem) -> bool:
if isinstance(elem, ProtoGetName):
return bool((self.targetname is None) or (elem.getname() == self.targetname))
return True
def _checkindex(self) -> bool:
if self.targetindex is not None:
self.index = self.index + 1
return bool(self.targetindex == (self.index - 1))
return True
def _checkvalue(self, elem: T_Elem) -> bool:
if isinstance(elem, ProtoGetVal):
return bool((self.targetvalue is None) or (elem.getvalue() == self.targetvalue))
return True
def walk(self, walklist: List[T_Elem], action: Callable) -> None:
"""walk a list using defined criteria
Args:
walklist: the list to walk on
action: a callable appyed to the found elements
"""
self.reset()
for elem in walklist:
# checking elem type
if len(self.targettypes) != 0:
bFound = False
for targettype in self.targettypes:
if isinstance(elem, targettype):
bFound = True
break
if not bFound:
continue
if not self._checkname(elem):
continue
if not self._checkindex():
continue
if not self._checkvalue(elem):
continue
action(elem)
self.num_matched = self.num_matched + 1
if self.targetindex is not None:
return

View File

@@ -108,7 +108,7 @@ class Test_PySimpleINI_base(unittest.TestCase):
def test_deletekey(self):
# create copy of the file
testini = PySimpleINI(testdir_path / "testfiles/test_delete.ini")
testini = PySimpleINI(testdir_path / "testfiles/test_deleteKey.ini")
testini.filepath = testdir_path / "tmp/out.ini"
testini.writefile(False)
@@ -157,7 +157,7 @@ class Test_PySimpleINI_base(unittest.TestCase):
def test_deletesection(self):
# create copy of the file
testini = PySimpleINI(testdir_path / "testfiles/test_delete.ini")
testini = PySimpleINI(testdir_path / "testfiles/test_deleteSection.ini")
testini.filepath = testdir_path / "tmp/out.ini"
testini.writefile(False)
@@ -195,7 +195,7 @@ class Test_PySimpleINI_base(unittest.TestCase):
def test_deletekey_fromfile(self):
# create copy of the file
testini = PySimpleINI(testdir_path / "testfiles/test_delete.ini")
testini = PySimpleINI(testdir_path / "testfiles/test_deleteSection.ini")
testini.filepath = testdir_path / "tmp/out.ini"
testini.writefile(False)
@@ -230,7 +230,7 @@ class Test_PySimpleINI_base(unittest.TestCase):
testinitmp.delkey_ex("testsection1", "key2", 1, "test2")
# create copy of the file
testini = PySimpleINI(testdir_path / "testfiles/test_delete.ini")
testini = PySimpleINI(testdir_path / "testfiles/test_deleteSection.ini")
testini.filepath = testdir_path / "tmp/out.ini"
testini.writefile(False)

View File

@@ -8,5 +8,4 @@ key2=test3
key1=test1
key2=test2
key2=test3
key2=test2
key2=test2

View File

@@ -0,0 +1,14 @@
[testsection1]
key1=test1
key2=test2
key2=test3
[testsection2]
key1=test1
key2=test2
key2=test3
key2=test2
[testsection2]
key6=test6