1# pyenchant
2#
3# Copyright (C) 2004-2008, Ryan Kelly
4#
5# This library is free software; you can redistribute it and/or
6# modify it under the terms of the GNU Lesser General Public
7# License as published by the Free Software Foundation; either
8# version 2.1 of the License, or (at your option) any later version.
9#
10# This library is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13# Lesser General Public License for more details.
14#
15# You should have received a copy of the GNU Lesser General Public
16# License along with this library; if not, write to the
17# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
18# Boston, MA 02111-1307, USA.
19#
20# In addition, as a special exception, you are
21# given permission to link the code of this program with
22# non-LGPL Spelling Provider libraries (eg: a MSFT Office
23# spell checker backend) and distribute linked combinations including
24# the two.  You must obey the GNU Lesser General Public License in all
25# respects for all of the code used other than said providers.  If you modify
26# this file, you may extend this exception to your version of the
27# file, but you are not obligated to do so.  If you do not wish to
28# do so, delete this exception statement from your version.
29#
30
31"""
32
33    enchant._enchant:  ctypes-based wrapper for enchant C library
34
35    This module implements the low-level interface to the underlying
36    C library for enchant.  The interface is based on ctypes and tries
37    to do as little as possible while making the higher-level components
38    easier to write.
39
40    The following conveniences are provided that differ from the underlying
41    C API:
42
43        * the "enchant" prefix has been removed from all functions, since
44          python has a proper module system
45        * callback functions do not take a user_data argument, since
46          python has proper closures that can manage this internally
47        * string lengths are not passed into functions such as dict_check,
48          since python strings know how long they are
49
50"""
51
52import sys
53import os
54import os.path
55import ctypes
56from ctypes import c_char_p, c_int, c_size_t, c_void_p, pointer, CFUNCTYPE, POINTER
57import ctypes.util
58import platform
59import textwrap
60
61
62def from_prefix(prefix):
63    find_message("finding from prefix ", prefix)
64    assert os.path.exists(prefix), prefix + "  does not exist"
65    bin_path = os.path.join(prefix, "bin")
66    enchant_dll_path = os.path.join(bin_path, "libenchant-2.dll")
67    assert os.path.exists(enchant_dll_path), enchant_dll_path + " does not exist"
68    # Make sure all the dlls found next to libenchant-2.dll
69    # (libglib-2.0-0.dll, libgmodule-2.0-0.dll, ...) can be
70    # used without having to modify %PATH%
71    new_path = bin_path + os.pathsep + os.environ["PATH"]
72    find_message("Prepending ", bin_path, " to %PATH%")
73    os.environ["PATH"] = new_path
74    return enchant_dll_path
75
76
77def from_env_var(library_path):
78    find_message("using PYENCHANT_LIBRARY_PATH env var")
79    assert os.path.exists(library_path), library_path + " does not exist"
80    return library_path
81
82
83def from_package_resources():
84    if sys.platform != "win32":
85        return None
86    bits, _ = platform.architecture()
87    if bits == "64bit":
88        subdir = "mingw64"  # hopefully this is compatible
89    else:
90        subdir = "mingw32"  # ditto
91    this_path = os.path.dirname(os.path.abspath(__file__))
92    data_path = os.path.join(this_path, "data", subdir)
93    find_message("looking in ", data_path)
94    if os.path.exists(data_path):
95        return from_prefix(data_path)
96
97
98def from_system():
99    # Note: keep enchant-2 first
100    find_message("looking in system")
101    candidates = [
102        "enchant-2",
103        "libenchant-2",
104        "enchant",
105        "libenchant",
106        "enchant-1",
107        "libenchant-1",
108    ]
109
110    for name in candidates:
111        find_message("with name ", name)
112        res = ctypes.util.find_library(name)
113        if res:
114            return res
115
116
117VERBOSE_FIND = False
118
119
120def find_message(*args):
121    if not VERBOSE_FIND:
122        return
123    print("pyenchant:: ", *args, sep="")
124
125
126def find_c_enchant_lib():
127    verbose = os.environ.get("PYENCHANT_VERBOSE_FIND")
128    if verbose:
129        global VERBOSE_FIND
130        VERBOSE_FIND = True
131    prefix = os.environ.get("PYENCHANT_ENCHANT_PREFIX")
132    if prefix:
133        return from_prefix(prefix)
134
135    library_path = os.environ.get("PYENCHANT_LIBRARY_PATH")
136    if library_path:
137        return from_env_var(library_path)
138
139    from_package = from_package_resources()
140    if from_package:
141        return from_package
142
143    # Last chance
144    return from_system()
145
146
147enchant_lib_path = find_c_enchant_lib()
148
149if enchant_lib_path is None:
150    msg = textwrap.dedent(
151        """\
152        The 'enchant' C library was not found and maybe needs to be installed.
153        See  https://pyenchant.github.io/pyenchant/install.html
154        for details
155        """
156    )
157    raise ImportError(msg)
158
159
160find_message("loading library ", enchant_lib_path)
161e = ctypes.cdll.LoadLibrary(enchant_lib_path)
162
163# Always assume the found enchant C dll is inside
164# the correct directory layout
165prefix_dir = os.path.dirname(os.path.dirname(enchant_lib_path))
166if hasattr(e, "enchant_set_prefix_dir") and prefix_dir:
167    find_message("setting prefix ", prefix_dir)
168    e.enchant_set_prefix_dir(prefix_dir.encode())
169
170
171def callback(restype, *argtypes):
172    """Factory for generating callback function prototypes.
173
174    This is factored into a factory so I can easily change the definition
175    for experimentation or debugging.
176    """
177    return CFUNCTYPE(restype, *argtypes)
178
179
180t_broker_desc_func = callback(None, c_char_p, c_char_p, c_char_p, c_void_p)
181t_dict_desc_func = callback(None, c_char_p, c_char_p, c_char_p, c_char_p, c_void_p)
182
183
184# Simple typedefs for readability
185
186t_broker = c_void_p
187t_dict = c_void_p
188
189
190# Now we can define the types of each function we are going to use
191
192broker_init = e.enchant_broker_init
193broker_init.argtypes = []
194broker_init.restype = t_broker
195
196broker_free = e.enchant_broker_free
197broker_free.argtypes = [t_broker]
198broker_free.restype = None
199
200broker_request_dict = e.enchant_broker_request_dict
201broker_request_dict.argtypes = [t_broker, c_char_p]
202broker_request_dict.restype = t_dict
203
204broker_request_pwl_dict = e.enchant_broker_request_pwl_dict
205broker_request_pwl_dict.argtypes = [t_broker, c_char_p]
206broker_request_pwl_dict.restype = t_dict
207
208broker_free_dict = e.enchant_broker_free_dict
209broker_free_dict.argtypes = [t_broker, t_dict]
210broker_free_dict.restype = None
211
212broker_dict_exists = e.enchant_broker_dict_exists
213broker_dict_exists.argtypes = [t_broker, c_char_p]
214broker_dict_exists.restype = c_int
215
216broker_set_ordering = e.enchant_broker_set_ordering
217broker_set_ordering.argtypes = [t_broker, c_char_p, c_char_p]
218broker_set_ordering.restype = None
219
220broker_get_error = e.enchant_broker_get_error
221broker_get_error.argtypes = [t_broker]
222broker_get_error.restype = c_char_p
223
224broker_describe1 = e.enchant_broker_describe
225broker_describe1.argtypes = [t_broker, t_broker_desc_func, c_void_p]
226broker_describe1.restype = None
227
228
229def broker_describe(broker, cbfunc):
230    def cbfunc1(*args):
231        cbfunc(*args[:-1])
232
233    broker_describe1(broker, t_broker_desc_func(cbfunc1), None)
234
235
236broker_list_dicts1 = e.enchant_broker_list_dicts
237broker_list_dicts1.argtypes = [t_broker, t_dict_desc_func, c_void_p]
238broker_list_dicts1.restype = None
239
240
241def broker_list_dicts(broker, cbfunc):
242    def cbfunc1(*args):
243        cbfunc(*args[:-1])
244
245    broker_list_dicts1(broker, t_dict_desc_func(cbfunc1), None)
246
247
248try:
249    broker_get_param = e.enchant_broker_get_param
250except AttributeError:
251    #  Make the lookup error occur at runtime
252    def broker_get_param(broker, name):
253        return e.enchant_broker_get_param(broker, name)
254
255
256else:
257    broker_get_param.argtypes = [t_broker, c_char_p]
258    broker_get_param.restype = c_char_p
259
260try:
261    broker_set_param = e.enchant_broker_set_param
262except AttributeError:
263    #  Make the lookup error occur at runtime
264    def broker_set_param(broker, name, value):
265        return e.enchant_broker_set_param(broker, name, value)
266
267
268else:
269    broker_set_param.argtypes = [t_broker, c_char_p, c_char_p]
270    broker_set_param.restype = None
271
272try:
273    get_version = e.enchant_get_version
274except AttributeError:
275    #  Make the lookup error occur at runtime
276    def get_version():
277        return e.enchant_get_version()
278
279
280else:
281    get_version.argtypes = []
282    get_version.restype = c_char_p
283
284try:
285    set_prefix_dir = e.enchant_set_prefix_dir
286except AttributeError:
287    #  Make the lookup error occur at runtime
288    def set_prefix_dir(path):
289        return e.enchant_set_prefix_dir(path)
290
291
292else:
293    set_prefix_dir.argtypes = [c_char_p]
294    set_prefix_dir.restype = None
295
296try:
297    get_user_config_dir = e.enchant_get_user_config_dir
298except AttributeError:
299    #  Make the lookup error occur at runtime
300    def get_user_config_dir():
301        return e.enchant_get_user_config_dir()
302
303
304else:
305    get_user_config_dir.argtypes = []
306    get_user_config_dir.restype = c_char_p
307
308dict_check1 = e.enchant_dict_check
309dict_check1.argtypes = [t_dict, c_char_p, c_size_t]
310dict_check1.restype = c_int
311
312
313def dict_check(dict, word):
314    return dict_check1(dict, word, len(word))
315
316
317dict_suggest1 = e.enchant_dict_suggest
318dict_suggest1.argtypes = [t_dict, c_char_p, c_size_t, POINTER(c_size_t)]
319dict_suggest1.restype = POINTER(c_char_p)
320
321
322def dict_suggest(dict, word):
323    num_suggs_p = pointer(c_size_t(0))
324    suggs_c = dict_suggest1(dict, word, len(word), num_suggs_p)
325    suggs = []
326    n = 0
327    while n < num_suggs_p.contents.value:
328        suggs.append(suggs_c[n])
329        n = n + 1
330    if num_suggs_p.contents.value > 0:
331        dict_free_string_list(dict, suggs_c)
332    return suggs
333
334
335dict_add1 = e.enchant_dict_add
336dict_add1.argtypes = [t_dict, c_char_p, c_size_t]
337dict_add1.restype = None
338
339
340def dict_add(dict, word):
341    return dict_add1(dict, word, len(word))
342
343
344dict_add_to_pwl1 = e.enchant_dict_add
345dict_add_to_pwl1.argtypes = [t_dict, c_char_p, c_size_t]
346dict_add_to_pwl1.restype = None
347
348
349def dict_add_to_pwl(dict, word):
350    return dict_add_to_pwl1(dict, word, len(word))
351
352
353dict_add_to_session1 = e.enchant_dict_add_to_session
354dict_add_to_session1.argtypes = [t_dict, c_char_p, c_size_t]
355dict_add_to_session1.restype = None
356
357
358def dict_add_to_session(dict, word):
359    return dict_add_to_session1(dict, word, len(word))
360
361
362dict_remove1 = e.enchant_dict_remove
363dict_remove1.argtypes = [t_dict, c_char_p, c_size_t]
364dict_remove1.restype = None
365
366
367def dict_remove(dict, word):
368    return dict_remove1(dict, word, len(word))
369
370
371dict_remove_from_session1 = e.enchant_dict_remove_from_session
372dict_remove_from_session1.argtypes = [t_dict, c_char_p, c_size_t]
373dict_remove_from_session1.restype = c_int
374
375
376def dict_remove_from_session(dict, word):
377    return dict_remove_from_session1(dict, word, len(word))
378
379
380dict_is_added1 = e.enchant_dict_is_added
381dict_is_added1.argtypes = [t_dict, c_char_p, c_size_t]
382dict_is_added1.restype = c_int
383
384
385def dict_is_added(dict, word):
386    return dict_is_added1(dict, word, len(word))
387
388
389dict_is_removed1 = e.enchant_dict_is_removed
390dict_is_removed1.argtypes = [t_dict, c_char_p, c_size_t]
391dict_is_removed1.restype = c_int
392
393
394def dict_is_removed(dict, word):
395    return dict_is_removed1(dict, word, len(word))
396
397
398dict_store_replacement1 = e.enchant_dict_store_replacement
399dict_store_replacement1.argtypes = [t_dict, c_char_p, c_size_t, c_char_p, c_size_t]
400dict_store_replacement1.restype = None
401
402
403def dict_store_replacement(dict, mis, cor):
404    return dict_store_replacement1(dict, mis, len(mis), cor, len(cor))
405
406
407dict_free_string_list = e.enchant_dict_free_string_list
408dict_free_string_list.argtypes = [t_dict, POINTER(c_char_p)]
409dict_free_string_list.restype = None
410
411dict_get_error = e.enchant_dict_get_error
412dict_get_error.argtypes = [t_dict]
413dict_get_error.restype = c_char_p
414
415dict_describe1 = e.enchant_dict_describe
416dict_describe1.argtypes = [t_dict, t_dict_desc_func, c_void_p]
417dict_describe1.restype = None
418
419
420def dict_describe(dict, cbfunc):
421    def cbfunc1(tag, name, desc, file, data):
422        cbfunc(tag, name, desc, file)
423
424    dict_describe1(dict, t_dict_desc_func(cbfunc1), None)
425