mirror of
https://github.com/llvm/llvm-project.git
synced 2025-04-26 06:36:07 +00:00
[python] Support PathLike filenames and directories
Python 3.6 introduced a file system path protocol (PEP 519[1]). The standard library APIs accepting file system paths now accept path objects too. It could be useful to add this here as well for convenience. [1] https://www.python.org/dev/peps/pep-0519 Authored by: jstasiak (Jakub Stasiak) Differential Revision: https://reviews.llvm.org/D54120 llvm-svn: 346586
This commit is contained in:
parent
e105b655a2
commit
248cf96547
@ -67,6 +67,7 @@ import collections
|
|||||||
|
|
||||||
import clang.enumerations
|
import clang.enumerations
|
||||||
|
|
||||||
|
import os
|
||||||
import sys
|
import sys
|
||||||
if sys.version_info[0] == 3:
|
if sys.version_info[0] == 3:
|
||||||
# Python 3 strings are unicode, translate them to/from utf8 for C-interop.
|
# Python 3 strings are unicode, translate them to/from utf8 for C-interop.
|
||||||
@ -123,6 +124,14 @@ elif sys.version_info[0] == 2:
|
|||||||
def b(x):
|
def b(x):
|
||||||
return x
|
return x
|
||||||
|
|
||||||
|
# We only support PathLike objects on Python version with os.fspath present
|
||||||
|
# to be consistent with the Python standard library. On older Python versions
|
||||||
|
# we only support strings and we have dummy fspath to just pass them through.
|
||||||
|
try:
|
||||||
|
fspath = os.fspath
|
||||||
|
except AttributeError:
|
||||||
|
def fspath(x):
|
||||||
|
return x
|
||||||
|
|
||||||
# ctypes doesn't implicitly convert c_void_p to the appropriate wrapper
|
# ctypes doesn't implicitly convert c_void_p to the appropriate wrapper
|
||||||
# object. This is a problem, because it means that from_parameter will see an
|
# object. This is a problem, because it means that from_parameter will see an
|
||||||
@ -2752,11 +2761,11 @@ class TranslationUnit(ClangObject):
|
|||||||
etc. e.g. ["-Wall", "-I/path/to/include"].
|
etc. e.g. ["-Wall", "-I/path/to/include"].
|
||||||
|
|
||||||
In-memory file content can be provided via unsaved_files. This is an
|
In-memory file content can be provided via unsaved_files. This is an
|
||||||
iterable of 2-tuples. The first element is the str filename. The
|
iterable of 2-tuples. The first element is the filename (str or
|
||||||
second element defines the content. Content can be provided as str
|
PathLike). The second element defines the content. Content can be
|
||||||
source code or as file objects (anything with a read() method). If
|
provided as str source code or as file objects (anything with a read()
|
||||||
a file object is being used, content will be read until EOF and the
|
method). If a file object is being used, content will be read until EOF
|
||||||
read cursor will not be reset to its original position.
|
and the read cursor will not be reset to its original position.
|
||||||
|
|
||||||
options is a bitwise or of TranslationUnit.PARSE_XXX flags which will
|
options is a bitwise or of TranslationUnit.PARSE_XXX flags which will
|
||||||
control parsing behavior.
|
control parsing behavior.
|
||||||
@ -2801,11 +2810,13 @@ class TranslationUnit(ClangObject):
|
|||||||
if hasattr(contents, "read"):
|
if hasattr(contents, "read"):
|
||||||
contents = contents.read()
|
contents = contents.read()
|
||||||
|
|
||||||
unsaved_array[i].name = b(name)
|
unsaved_array[i].name = b(fspath(name))
|
||||||
unsaved_array[i].contents = b(contents)
|
unsaved_array[i].contents = b(contents)
|
||||||
unsaved_array[i].length = len(contents)
|
unsaved_array[i].length = len(contents)
|
||||||
|
|
||||||
ptr = conf.lib.clang_parseTranslationUnit(index, filename, args_array,
|
ptr = conf.lib.clang_parseTranslationUnit(index,
|
||||||
|
fspath(filename) if filename is not None else None,
|
||||||
|
args_array,
|
||||||
len(args), unsaved_array,
|
len(args), unsaved_array,
|
||||||
len(unsaved_files), options)
|
len(unsaved_files), options)
|
||||||
|
|
||||||
@ -2826,11 +2837,13 @@ class TranslationUnit(ClangObject):
|
|||||||
|
|
||||||
index is optional and is the Index instance to use. If not provided,
|
index is optional and is the Index instance to use. If not provided,
|
||||||
a default Index will be created.
|
a default Index will be created.
|
||||||
|
|
||||||
|
filename can be str or PathLike.
|
||||||
"""
|
"""
|
||||||
if index is None:
|
if index is None:
|
||||||
index = Index.create()
|
index = Index.create()
|
||||||
|
|
||||||
ptr = conf.lib.clang_createTranslationUnit(index, filename)
|
ptr = conf.lib.clang_createTranslationUnit(index, fspath(filename))
|
||||||
if not ptr:
|
if not ptr:
|
||||||
raise TranslationUnitLoadError(filename)
|
raise TranslationUnitLoadError(filename)
|
||||||
|
|
||||||
@ -2983,7 +2996,7 @@ class TranslationUnit(ClangObject):
|
|||||||
print(value)
|
print(value)
|
||||||
if not isinstance(value, str):
|
if not isinstance(value, str):
|
||||||
raise TypeError('Unexpected unsaved file contents.')
|
raise TypeError('Unexpected unsaved file contents.')
|
||||||
unsaved_files_array[i].name = name
|
unsaved_files_array[i].name = fspath(name)
|
||||||
unsaved_files_array[i].contents = value
|
unsaved_files_array[i].contents = value
|
||||||
unsaved_files_array[i].length = len(value)
|
unsaved_files_array[i].length = len(value)
|
||||||
ptr = conf.lib.clang_reparseTranslationUnit(self, len(unsaved_files),
|
ptr = conf.lib.clang_reparseTranslationUnit(self, len(unsaved_files),
|
||||||
@ -3002,10 +3015,10 @@ class TranslationUnit(ClangObject):
|
|||||||
case, the reason(s) why should be available via
|
case, the reason(s) why should be available via
|
||||||
TranslationUnit.diagnostics().
|
TranslationUnit.diagnostics().
|
||||||
|
|
||||||
filename -- The path to save the translation unit to.
|
filename -- The path to save the translation unit to (str or PathLike).
|
||||||
"""
|
"""
|
||||||
options = conf.lib.clang_defaultSaveOptions(self)
|
options = conf.lib.clang_defaultSaveOptions(self)
|
||||||
result = int(conf.lib.clang_saveTranslationUnit(self, filename,
|
result = int(conf.lib.clang_saveTranslationUnit(self, fspath(filename),
|
||||||
options))
|
options))
|
||||||
if result != 0:
|
if result != 0:
|
||||||
raise TranslationUnitSaveError(result,
|
raise TranslationUnitSaveError(result,
|
||||||
@ -3047,10 +3060,10 @@ class TranslationUnit(ClangObject):
|
|||||||
print(value)
|
print(value)
|
||||||
if not isinstance(value, str):
|
if not isinstance(value, str):
|
||||||
raise TypeError('Unexpected unsaved file contents.')
|
raise TypeError('Unexpected unsaved file contents.')
|
||||||
unsaved_files_array[i].name = b(name)
|
unsaved_files_array[i].name = b(fspath(name))
|
||||||
unsaved_files_array[i].contents = b(value)
|
unsaved_files_array[i].contents = b(value)
|
||||||
unsaved_files_array[i].length = len(value)
|
unsaved_files_array[i].length = len(value)
|
||||||
ptr = conf.lib.clang_codeCompleteAt(self, path, line, column,
|
ptr = conf.lib.clang_codeCompleteAt(self, fspath(path), line, column,
|
||||||
unsaved_files_array, len(unsaved_files), options)
|
unsaved_files_array, len(unsaved_files), options)
|
||||||
if ptr:
|
if ptr:
|
||||||
return CodeCompletionResults(ptr)
|
return CodeCompletionResults(ptr)
|
||||||
@ -3078,7 +3091,7 @@ class File(ClangObject):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def from_name(translation_unit, file_name):
|
def from_name(translation_unit, file_name):
|
||||||
"""Retrieve a file handle within the given translation unit."""
|
"""Retrieve a file handle within the given translation unit."""
|
||||||
return File(conf.lib.clang_getFile(translation_unit, file_name))
|
return File(conf.lib.clang_getFile(translation_unit, fspath(file_name)))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
@ -3229,7 +3242,7 @@ class CompilationDatabase(ClangObject):
|
|||||||
"""Builds a CompilationDatabase from the database found in buildDir"""
|
"""Builds a CompilationDatabase from the database found in buildDir"""
|
||||||
errorCode = c_uint()
|
errorCode = c_uint()
|
||||||
try:
|
try:
|
||||||
cdb = conf.lib.clang_CompilationDatabase_fromDirectory(buildDir,
|
cdb = conf.lib.clang_CompilationDatabase_fromDirectory(fspath(buildDir),
|
||||||
byref(errorCode))
|
byref(errorCode))
|
||||||
except CompilationDatabaseError as e:
|
except CompilationDatabaseError as e:
|
||||||
raise CompilationDatabaseError(int(errorCode.value),
|
raise CompilationDatabaseError(int(errorCode.value),
|
||||||
@ -3242,7 +3255,7 @@ class CompilationDatabase(ClangObject):
|
|||||||
build filename. Returns None if filename is not found in the database.
|
build filename. Returns None if filename is not found in the database.
|
||||||
"""
|
"""
|
||||||
return conf.lib.clang_CompilationDatabase_getCompileCommands(self,
|
return conf.lib.clang_CompilationDatabase_getCompileCommands(self,
|
||||||
filename)
|
fspath(filename))
|
||||||
|
|
||||||
def getAllCompileCommands(self):
|
def getAllCompileCommands(self):
|
||||||
"""
|
"""
|
||||||
@ -4090,7 +4103,7 @@ class Config:
|
|||||||
raise Exception("library path must be set before before using " \
|
raise Exception("library path must be set before before using " \
|
||||||
"any other functionalities in libclang.")
|
"any other functionalities in libclang.")
|
||||||
|
|
||||||
Config.library_path = path
|
Config.library_path = fspath(path)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def set_library_file(filename):
|
def set_library_file(filename):
|
||||||
@ -4099,7 +4112,7 @@ class Config:
|
|||||||
raise Exception("library file must be set before before using " \
|
raise Exception("library file must be set before before using " \
|
||||||
"any other functionalities in libclang.")
|
"any other functionalities in libclang.")
|
||||||
|
|
||||||
Config.library_file = filename
|
Config.library_file = fspath(filename)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def set_compatibility_check(check_status):
|
def set_compatibility_check(check_status):
|
||||||
|
@ -11,6 +11,8 @@ import os
|
|||||||
import gc
|
import gc
|
||||||
import unittest
|
import unittest
|
||||||
import sys
|
import sys
|
||||||
|
from .util import skip_if_no_fspath
|
||||||
|
from .util import str_to_path
|
||||||
|
|
||||||
|
|
||||||
kInputsDir = os.path.join(os.path.dirname(__file__), 'INPUTS')
|
kInputsDir = os.path.join(os.path.dirname(__file__), 'INPUTS')
|
||||||
@ -37,6 +39,13 @@ class TestCDB(unittest.TestCase):
|
|||||||
cmds = cdb.getCompileCommands('/home/john.doe/MyProject/project.cpp')
|
cmds = cdb.getCompileCommands('/home/john.doe/MyProject/project.cpp')
|
||||||
self.assertNotEqual(len(cmds), 0)
|
self.assertNotEqual(len(cmds), 0)
|
||||||
|
|
||||||
|
@skip_if_no_fspath
|
||||||
|
def test_lookup_succeed_pathlike(self):
|
||||||
|
"""Same as test_lookup_succeed, but with PathLikes"""
|
||||||
|
cdb = CompilationDatabase.fromDirectory(str_to_path(kInputsDir))
|
||||||
|
cmds = cdb.getCompileCommands(str_to_path('/home/john.doe/MyProject/project.cpp'))
|
||||||
|
self.assertNotEqual(len(cmds), 0)
|
||||||
|
|
||||||
def test_all_compilecommand(self):
|
def test_all_compilecommand(self):
|
||||||
"""Check we get all results from the db"""
|
"""Check we get all results from the db"""
|
||||||
cdb = CompilationDatabase.fromDirectory(kInputsDir)
|
cdb = CompilationDatabase.fromDirectory(kInputsDir)
|
||||||
|
@ -6,6 +6,8 @@ if 'CLANG_LIBRARY_PATH' in os.environ:
|
|||||||
from clang.cindex import TranslationUnit
|
from clang.cindex import TranslationUnit
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
|
from .util import skip_if_no_fspath
|
||||||
|
from .util import str_to_path
|
||||||
|
|
||||||
|
|
||||||
class TestCodeCompletion(unittest.TestCase):
|
class TestCodeCompletion(unittest.TestCase):
|
||||||
@ -43,6 +45,32 @@ void f() {
|
|||||||
]
|
]
|
||||||
self.check_completion_results(cr, expected)
|
self.check_completion_results(cr, expected)
|
||||||
|
|
||||||
|
@skip_if_no_fspath
|
||||||
|
def test_code_complete_pathlike(self):
|
||||||
|
files = [(str_to_path('fake.c'), """
|
||||||
|
/// Aaa.
|
||||||
|
int test1;
|
||||||
|
|
||||||
|
/// Bbb.
|
||||||
|
void test2(void);
|
||||||
|
|
||||||
|
void f() {
|
||||||
|
|
||||||
|
}
|
||||||
|
""")]
|
||||||
|
|
||||||
|
tu = TranslationUnit.from_source(str_to_path('fake.c'), ['-std=c99'], unsaved_files=files,
|
||||||
|
options=TranslationUnit.PARSE_INCLUDE_BRIEF_COMMENTS_IN_CODE_COMPLETION)
|
||||||
|
|
||||||
|
cr = tu.codeComplete(str_to_path('fake.c'), 9, 1, unsaved_files=files, include_brief_comments=True)
|
||||||
|
|
||||||
|
expected = [
|
||||||
|
"{'int', ResultType} | {'test1', TypedText} || Priority: 50 || Availability: Available || Brief comment: Aaa.",
|
||||||
|
"{'void', ResultType} | {'test2', TypedText} | {'(', LeftParen} | {')', RightParen} || Priority: 50 || Availability: Available || Brief comment: Bbb.",
|
||||||
|
"{'return', TypedText} || Priority: 40 || Availability: Available || Brief comment: None"
|
||||||
|
]
|
||||||
|
self.check_completion_results(cr, expected)
|
||||||
|
|
||||||
def test_code_complete_availability(self):
|
def test_code_complete_availability(self):
|
||||||
files = [('fake.cpp', """
|
files = [('fake.cpp', """
|
||||||
class P {
|
class P {
|
||||||
|
@ -20,6 +20,8 @@ from clang.cindex import TranslationUnitLoadError
|
|||||||
from clang.cindex import TranslationUnit
|
from clang.cindex import TranslationUnit
|
||||||
from .util import get_cursor
|
from .util import get_cursor
|
||||||
from .util import get_tu
|
from .util import get_tu
|
||||||
|
from .util import skip_if_no_fspath
|
||||||
|
from .util import str_to_path
|
||||||
|
|
||||||
|
|
||||||
kInputsDir = os.path.join(os.path.dirname(__file__), 'INPUTS')
|
kInputsDir = os.path.join(os.path.dirname(__file__), 'INPUTS')
|
||||||
@ -36,6 +38,17 @@ def save_tu(tu):
|
|||||||
yield t.name
|
yield t.name
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def save_tu_pathlike(tu):
|
||||||
|
"""Convenience API to save a TranslationUnit to a file.
|
||||||
|
|
||||||
|
Returns the filename it was saved to.
|
||||||
|
"""
|
||||||
|
with tempfile.NamedTemporaryFile() as t:
|
||||||
|
tu.save(str_to_path(t.name))
|
||||||
|
yield t.name
|
||||||
|
|
||||||
|
|
||||||
class TestTranslationUnit(unittest.TestCase):
|
class TestTranslationUnit(unittest.TestCase):
|
||||||
def test_spelling(self):
|
def test_spelling(self):
|
||||||
path = os.path.join(kInputsDir, 'hello.cpp')
|
path = os.path.join(kInputsDir, 'hello.cpp')
|
||||||
@ -89,6 +102,22 @@ int SOME_DEFINE;
|
|||||||
spellings = [c.spelling for c in tu.cursor.get_children()]
|
spellings = [c.spelling for c in tu.cursor.get_children()]
|
||||||
self.assertEqual(spellings[-1], 'x')
|
self.assertEqual(spellings[-1], 'x')
|
||||||
|
|
||||||
|
@skip_if_no_fspath
|
||||||
|
def test_from_source_accepts_pathlike(self):
|
||||||
|
tu = TranslationUnit.from_source(str_to_path('fake.c'), ['-Iincludes'], unsaved_files = [
|
||||||
|
(str_to_path('fake.c'), """
|
||||||
|
#include "fake.h"
|
||||||
|
int x;
|
||||||
|
int SOME_DEFINE;
|
||||||
|
"""),
|
||||||
|
(str_to_path('includes/fake.h'), """
|
||||||
|
#define SOME_DEFINE y
|
||||||
|
""")
|
||||||
|
])
|
||||||
|
spellings = [c.spelling for c in tu.cursor.get_children()]
|
||||||
|
self.assertEqual(spellings[-2], 'x')
|
||||||
|
self.assertEqual(spellings[-1], 'y')
|
||||||
|
|
||||||
def assert_normpaths_equal(self, path1, path2):
|
def assert_normpaths_equal(self, path1, path2):
|
||||||
""" Compares two paths for equality after normalizing them with
|
""" Compares two paths for equality after normalizing them with
|
||||||
os.path.normpath
|
os.path.normpath
|
||||||
@ -135,6 +164,16 @@ int SOME_DEFINE;
|
|||||||
self.assertTrue(os.path.exists(path))
|
self.assertTrue(os.path.exists(path))
|
||||||
self.assertGreater(os.path.getsize(path), 0)
|
self.assertGreater(os.path.getsize(path), 0)
|
||||||
|
|
||||||
|
@skip_if_no_fspath
|
||||||
|
def test_save_pathlike(self):
|
||||||
|
"""Ensure TranslationUnit.save() works with PathLike filename."""
|
||||||
|
|
||||||
|
tu = get_tu('int foo();')
|
||||||
|
|
||||||
|
with save_tu_pathlike(tu) as path:
|
||||||
|
self.assertTrue(os.path.exists(path))
|
||||||
|
self.assertGreater(os.path.getsize(path), 0)
|
||||||
|
|
||||||
def test_save_translation_errors(self):
|
def test_save_translation_errors(self):
|
||||||
"""Ensure that saving to an invalid directory raises."""
|
"""Ensure that saving to an invalid directory raises."""
|
||||||
|
|
||||||
@ -167,6 +206,22 @@ int SOME_DEFINE;
|
|||||||
# Just in case there is an open file descriptor somewhere.
|
# Just in case there is an open file descriptor somewhere.
|
||||||
del tu2
|
del tu2
|
||||||
|
|
||||||
|
@skip_if_no_fspath
|
||||||
|
def test_load_pathlike(self):
|
||||||
|
"""Ensure TranslationUnits can be constructed from saved files -
|
||||||
|
PathLike variant."""
|
||||||
|
tu = get_tu('int foo();')
|
||||||
|
self.assertEqual(len(tu.diagnostics), 0)
|
||||||
|
with save_tu(tu) as path:
|
||||||
|
tu2 = TranslationUnit.from_ast_file(filename=str_to_path(path))
|
||||||
|
self.assertEqual(len(tu2.diagnostics), 0)
|
||||||
|
|
||||||
|
foo = get_cursor(tu2, 'foo')
|
||||||
|
self.assertIsNotNone(foo)
|
||||||
|
|
||||||
|
# Just in case there is an open file descriptor somewhere.
|
||||||
|
del tu2
|
||||||
|
|
||||||
def test_index_parse(self):
|
def test_index_parse(self):
|
||||||
path = os.path.join(kInputsDir, 'hello.cpp')
|
path = os.path.join(kInputsDir, 'hello.cpp')
|
||||||
index = Index.create()
|
index = Index.create()
|
||||||
@ -185,6 +240,19 @@ int SOME_DEFINE;
|
|||||||
with self.assertRaises(Exception):
|
with self.assertRaises(Exception):
|
||||||
f = tu.get_file('foobar.cpp')
|
f = tu.get_file('foobar.cpp')
|
||||||
|
|
||||||
|
@skip_if_no_fspath
|
||||||
|
def test_get_file_pathlike(self):
|
||||||
|
"""Ensure tu.get_file() works appropriately with PathLike filenames."""
|
||||||
|
|
||||||
|
tu = get_tu('int foo();')
|
||||||
|
|
||||||
|
f = tu.get_file(str_to_path('t.c'))
|
||||||
|
self.assertIsInstance(f, File)
|
||||||
|
self.assertEqual(f.name, 't.c')
|
||||||
|
|
||||||
|
with self.assertRaises(Exception):
|
||||||
|
f = tu.get_file(str_to_path('foobar.cpp'))
|
||||||
|
|
||||||
def test_get_source_location(self):
|
def test_get_source_location(self):
|
||||||
"""Ensure tu.get_source_location() works."""
|
"""Ensure tu.get_source_location() works."""
|
||||||
|
|
||||||
|
@ -1,5 +1,15 @@
|
|||||||
# This file provides common utility functions for the test suite.
|
# This file provides common utility functions for the test suite.
|
||||||
|
|
||||||
|
import os
|
||||||
|
HAS_FSPATH = hasattr(os, 'fspath')
|
||||||
|
|
||||||
|
if HAS_FSPATH:
|
||||||
|
from pathlib import Path as str_to_path
|
||||||
|
else:
|
||||||
|
str_to_path = None
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
|
||||||
from clang.cindex import Cursor
|
from clang.cindex import Cursor
|
||||||
from clang.cindex import TranslationUnit
|
from clang.cindex import TranslationUnit
|
||||||
|
|
||||||
@ -68,8 +78,13 @@ def get_cursors(source, spelling):
|
|||||||
return cursors
|
return cursors
|
||||||
|
|
||||||
|
|
||||||
|
skip_if_no_fspath = unittest.skipUnless(HAS_FSPATH,
|
||||||
|
"Requires file system path protocol / Python 3.6+")
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'get_cursor',
|
'get_cursor',
|
||||||
'get_cursors',
|
'get_cursors',
|
||||||
'get_tu',
|
'get_tu',
|
||||||
|
'skip_if_no_fspath',
|
||||||
|
'str_to_path',
|
||||||
]
|
]
|
||||||
|
Loading…
x
Reference in New Issue
Block a user