How to test if object is a pathlib path? - python-3.x

I want to test if obj is a pathlib path and realized that the condition type(obj) is pathlib.PosixPath will be False for a path generated on a Windows machine.
Thus the question, is there a way to test if an object is a pathlib path (any of the possible, Path, PosixPath, WindowsPath, or the Pure...-analogs) without checking for all 6 version explicitly?

Yes, using isinstance(). Some sample code:
# Python 3.4+
import pathlib
path = pathlib.Path("foo/test.txt")
# path = pathlib.PureWindowsPath(r'C:\foo\file.txt')
# checks if the variable is any instance of pathlib
if isinstance(path, pathlib.PurePath):
print("It's pathlib!")
# No PurePath
if isinstance(path, pathlib.Path):
print("No Pure path found here")
if isinstance(path, pathlib.WindowsPath):
print("We're on Windows")
elif isinstance(path, pathlib.PosixPath):
print("We're on Linux / Mac")
# PurePath
else:
print("We're a Pure path")
Why does isinstance(path, pathlib.PurePath) work for all types? Take a look at this diagram:
We see that PurePath is at the top, that means everything else is a subclass of it. Therefore, we only have to check this one.
Same reasoning for Path to check non-pure Paths.
Bonus: You can use a tuple in isinstance(path, (pathlib.WindowsPath, pathlib.PosixPath)) to check 2 types at once.

I liked NumesSanguis answer and this is how I used what I learnt:
def check_path_instance(obj: object, name: str) -> pathlib.Path:
""" Check path instance type then convert and return
:param obj: object to check and convert
:param name: name of the object to check (apparently there is no sane way to get the name of the variable)
:return: pathlib.Path of the object else exit the programe with critical error
"""
if isinstance(obj, (pathlib.WindowsPath, pathlib.PosixPath)):
return pathlib.Path(obj)
else:
if isinstance(obj, str):
return pathlib.Path(str(obj))
else:
logging.critical(
f'{name} type is: {type(obj)}, not pathlib.WindowsPath or pathlib.PosixPath or str')
)
sys.exit(1)

Related

ValueError: data_class [Twist] is not a class

I am trying to get values from .yaml file for subscription and writing bag file in ROS. But i encounter with this error and i couldn't solve it. I guess I can't define my values as a class name in code.
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
import rospy
import rosbag
from geometry_msgs.msg import Twist
filename='test.bag'
bag = rosbag.Bag(filename, 'w')
sensor_info=rospy.get_param("/bag/sensor_info")
move_datatype=sensor_info[1]['datatype'] # Twist
move_topic=sensor_info[1]['topic_name'] # /cmd_vel
def move_callback(msg):
x=msg
def main():
global x
rospy.init_node('rosbag_save', anonymous=True)
rospy.Subscriber(move_topic,move_datatype(),move_callback)
while not rospy.is_shutdown():
bag.write(f'{move_topic}',x)
if __name__ == '__main__':
try:
main()
except rospy.ROSInterruptException:
pass
here is my code
bag:
sensor_info:
- {"topic_name": "/multi_scan", "datatype": "LaserScan"}
- {"topic_name": "/cmd_vel", "datatype": "Twist"}
and here is my .yaml file
You're getting this error because when you read the rosparam in from the server it will be stored as a string; which you can't pass to a subscriber. Instead you should convert it to the object type after reading it in. There are a couple of ways to do this, but using globals() will probably be the easiest. Essentially this returns a dictionary of everything in the global symbol table and from there you can use a key(a string) to get a value(class type). This can be done such as:
sensor_info = rospy.get_param("/bag/sensor_info")
move_datatype_string = sensor_info[1]['datatype'] # Twist
move_topic = sensor_info[1]['topic_name'] # /cmd_vel
move_datatype = globals()[move_datatype_string]

Python argument type specification for files

I know, for example, that a function can be written to let know its argument is str:
def myfunc(s: str) -> None:
I search the documentation of typing but couldn't find anything about files as arguments.
How can I specify something as the following:
def use_file(a_file: ???) -> None:
where ??? is a binary file (as one created with open("data", "rb"))?
The typing module provides specific types for file-like objects. Your question is a duplicate of this question.
from typing import IO, TextIO, BinaryIO
# Answer to the question
# If your function only accepts files opened in binary mode
def use_file(a_file: BinaryIO) -> None:
...
# If your function only accepts files opened in text mode
def use_file(a_file: TextIO) -> None:
...
# If your function accepts files opened in both modes
def use_file(a_file: IO) -> None:
...

Type hinting a method that returns a string or error in Python 3.7+

I have a Python script that starts with a method searching for a CSV file in the current directory or downloads a directory to make some processing. If no CSV file is found, the program should not run and exit with an error message.
I type annotated a tentative method as follows:
import glob
import os
def get_csv_filename() -> str:
""" Returns first csv filename in current folder or Downloads folder """
csv_filenames = glob.glob('*.csv')
if csv_filenames:
return csv_filenames[0]
home = os.path.expanduser("~")
csv_filenames = glob.glob(home + "/Downloads/*.csv")
if csv_filenames:
return csv_filenames[0]
# If I don't use return, I also get problems with pylint
return exit("Error: no file found, check the documentation for more info.")
def main() -> None:
""" Reads a CSV and does some processing """
filename = get_csv_filename()
If I type check with eg. pytype I get the error:
get_csv_filename: bad option in return type [bad-return-type]
Expected: str
Actually returned: None
What would you recommend doing to make this code compliant?
This section of PEP 484 may be helpful. I don't have mypy or pytype installed to try it, but maybe this would work:
from typing import NoReturn
def get_csv_filename() -> str:
""" Returns first csv filename in current folder or Downloads folder """
csv_filenames = glob.glob('*.csv')
if csv_filenames:
return csv_filenames[0]
...
stop("Error: no file found, check the documentation for more info.")
return ""
def stop(msg) -> NoReturn:
exit(msg)
Another option would be:
from typing import Union
def get_csv_filename() -> Union[None, str]:
...
From the ideas of Steve Bremer's response, the problem can be solved with a simpler approach:
from typing import Optional
def get_csv_filename() -> Optional[str]:
...
In fact, Optional[something] is equivalent to Union[None, something].

Determine if a path is valid in a class constructor

Without violating the guideline that a constructor should do work, I need to determine if the provided string (destination_directory) is a valid path before assigning it in the constructor.
It doesn't have to exist, but the provide string must be a valid one, i.e. no invalid symbols, or illegal characters. My project will run on Windows only, not Linux.
I looked at this page, but the answers seem to try and open the directory to test if the provided string is valid.
I also tried os.path.isabs(path)but it doesn't provide the results I require. For example, it says that T:\\\\Pictures is a absolute path, while that may be true, the \\\\ should mean the path is invalid.
Is there a clean, perhaps one line way of achieving what I want?
def __init__(self, destination_directory: str)
self._validate_path(path=destination_directory)
self.destination_directory = destination_directory
def _validate_path(self, path)
# code to validate path should go here.
We now a few things about a path, it contains at least a drive letter and subdirectories.
We also have rules about what symbols are not allowed in directories. We also know that a drive letter contains a single character.
Instead of allowing users of our class to pass in a full path, we break it down and only allow valid strings for directories names and one letter for the drive. When everything is validated, we can use the os module to build our path.
Here is how I would structure my Folder class:
class Folder:
def __init__(self, *subdirectories, root_drive):
self._validate_drive_letter(letter = root_drive)
self._validate_path(path=subdirectories)
self._root_drive = root_drive
self._subdirectories = subdirectories
def _validate_drive_letter(self, letter):
if not letter or len(letter) > 2 or not letter.isalpha():
raise ValueError("Drive letter is invalid")
def _validate_path(self, path):
self._forbidden_characters = ["<", ">", ":", "/", '"', "|", "?", "*", '\\']
for character in path:
for item in character:
if item in self._forbidden_characters:
raise ValueError("Directory cannot contain invalid characters")
def construct_full_path(self) -> str:
# use the os module and constructor parameters to build a valid path
def __str__(self) -> str:
return f"Drive Letter: {self._root_drive} Subdirectories: {self._subdirectories}"
Main:
def main():
try:
portable_drive = Folder("Pictures", "Landscape", root_drive="R") # Valid
# Using the construct_full_path() function, the returned string would be:
# R:\Pictures\Landscape
# Notice the user doesn't provide the : or the \, the class will do it.
vacation_pictures = Folder("Vac??tion", root_drive="T") # Will raise ValueError
# If we fix the error and call construct_full_path() we will get T:\Vacation
except ValueError as error:
print(error)
else:
print(portable_drive)
print(vacation_pictures)
if __name__ == "__main__":
main()
It may not be the best approach, but it works. I know a nested for loop is bad, but I don't see any other way to validate the individual characters of a string.
A regex solution:
import re
windows_path_regex = re.compile(r"""
\A
(?:(?:[a-z]:|\\\\[a-z0-9_.$\●-]+\\[a-z0-9_.$\●-]+)\\| # Drive
\\?[^\\/:*?"<>|\r\n]+\\?) # Relative path
(?:[^\\/:*?"<>|\r\n]+\\)* # Folder
[^\\/:*?"<>|\r\n]* # File
\Z
""", re.VERBOSE|re.I)
d = windows_path_regex .match(r"\test\txt.txt")
print(bool(d))
Note that\ is a valid path but / is not.
I used 8.18. Validate Windows Paths as a reference.

Unit Testing a Method That Uses a Context Manager

I have a method I would like to unit test. The method expects a file path, which is then opened - using a context manager - to parse a value which is then returned, should it be present, simple enough.
#staticmethod
def read_in_target_language(file_path):
"""
.. note:: Language code attributes/values can occur
on either the first or the second line of bilingual.
"""
with codecs.open(file_path, 'r', encoding='utf-8') as source:
line_1, line_2 = next(source), next(source)
get_line_1 = re.search(
'(target-language=")(.+?)(")', line_1, re.IGNORECASE)
get_line_2 = re.search(
'(target-language=")(.+?)(")', line_2, re.IGNORECASE)
if get_line_1 is not None:
return get_line_1.group(2)
else:
return get_line_2.group(2)
I want to avoid testing against external files - for obvious reasons - and do not wish to create temp files. In addition, I cannot use StringIO in this case.
How can I mock the file_path object in my unit test case? Ultimately I would need to create a mock path that contains differing values. Any help is gratefully received.
(Disclaimer: I don't speak Python, so I'm likely to err in details)
I suggest that you instead mock codecs. Make the mock's open method return an object with test data to be returned from the read calls. That might involve creating another mock object for the return value; I don't know if there are some stock classes in Python that you could use for that purpose instead.
Then, in order to actually enable testing the logic, add a parameter to read_in_target_language that represents an object that can assume the role of the original codecs object, i.e. dependency injection by argument. For convenience I guess you could default it to codecs.
I'm not sure how far Python's duck typing goes with regards to static vs instance methods, but something like this should give you the general idea:
def read_in_target_language(file_path, opener=codecs):
...
with opener.open(file_path, 'r', encoding='utf-8') as source:
If the above isn't possible you could just add a layer of indirection:
class CodecsOpener:
...
def open(self, file_path, access, encoding):
return codecs.open(file_path, access, encoding)
class MockOpener:
...
def __init__(self, open_result):
self.open_result = open_result
def open(self, file_path, access, encoding):
return self.open_result
...
def read_in_target_language(file_path, opener=CodecsOpener()):
...
with opener.open(file_path, 'r', encoding='utf-8') as source:
...
...
def test():
readable_data = ...
opener = MockOpener(readable_data)
result = <class>.read_in_target_language('whatever', opener)
<check result>

Resources