Merge branch 'v4.0'

This commit is contained in:
kikito 2015-12-31 18:54:36 +01:00
commit 9cb11c4dd6
8 changed files with 304 additions and 280 deletions

View File

@ -10,12 +10,19 @@ env:
before_install:
- pip install hererocks
- hererocks base -r^ --$LUA
- export PATH=$PATH:$PWD/base/bin # Add directory with all installed binaries to PATH
- hererocks lua_install -r^ --$LUA
- export PATH=$PATH:$PWD/lua_install/bin # Add directory with all installed binaries to PATH
install:
- luarocks install busted
- luarocks install luacov
- luarocks install luacov-coveralls
script:
- busted -v
- busted --verbose --coverage
after_success:
- luacov-coveralls --exclude $TRAVIS_BUILD_DIR/lua_install
branches:
except:

View File

@ -1,6 +1,14 @@
middleclass changelog
====================
Version 4.0.0
* Unified the method and metamethod lookup into a single algorithm
* Added the capacity of setting up the `__index` metamethod in classes
* Removed global `Object` (classes created with `class(<name>)` have no superclass now)
* Removed default method `Class:implements(<mixin>)`
* Renamed several internal functions
Version 3.2.0
* Changed the way metamethods were handled to fix certain bugs (un-stubbed metamethods could not be inherited)

View File

@ -2,6 +2,7 @@ middleclass
===========
[![Build Status](https://travis-ci.org/kikito/middleclass.png?branch=master)](https://travis-ci.org/kikito/middleclass)
[![Coverage Status](https://coveralls.io/repos/kikito/middleclass/badge.svg?branch=master&service=github)](https://coveralls.io/github/kikito/middleclass?branch=master)
A simple OOP library for Lua. It has inheritance, metamethods (operators), class variables and weak mixin support.
@ -39,6 +40,10 @@ Documentation
See the [github wiki page](https://github.com/kikito/middleclass/wiki) for examples & documentation.
You can read the `CHANGELOG.md` file to see what has changed on each version of this library.
If you need help updating to a new middleclass version, read `UPDATING.md`.
Installation
============
@ -72,52 +77,4 @@ License
Middleclass is distributed under the MIT license.
Updating from 2.x
=================
Middleclass used to expose several global variables on the main scope. It does not do that anymore.
`class` is now returned by `require 'middleclass'`, and it is not set globally. So you can do this:
```lua
local class = require 'middleclass'
local MyClass = class('MyClass') -- works as before
```
`Object` is not a global variable any more. But you can get it from `class.Object`
```lua
local class = require 'middleclass'
local Object = class.Object
print(Object) -- prints 'class Object'
```
The public functions `instanceOf`, `subclassOf` and `includes` have been replaced by `Object.isInstanceOf`, `Object.static.isSubclassOf` and `Object.static.includes`.
Prior to 3.x:
```lua
instanceOf(MyClass, obj)
subclassOf(Object, aClass)
includes(aMixin, aClass)
```
Since 3.x:
```lua
obj:isInstanceOf(MyClass)
aClass:isSubclassOf(Object)
aClass:includes(aMixin)
```
The 3.x code snippet will throw an error if `obj` is not an object, or if `aClass` is not a class (since they will not implement `isInstanceOf`, `isSubclassOf` or `includes`).
If you are unsure of whether `obj` and `aClass` are an object or a class, you can use the methods in `Object`. They are prepared to work with random types, not just classes and instances:
```lua
Object.isInstanceOf(obj, MyClass)
Object.isSubclassOf(aClass, Object)
Object.includes(aClass, aMixin)
```
Notice that the parameter order is not the same now as it was in 2.x. Also note the change in naming: `isInstanceOf` instead of `instanceOf`, and `isSubclassOf` instead of `subclassOf`.

69
UPDATING.md Normal file
View File

@ -0,0 +1,69 @@
Updating from 3.x to 4.x
========================
In middleclass 4.0 there is no global `Object` class any more. Classes created with `class(<name>)` don't have a superclass any more.
If you need a global `Object` class, you must create it explicitly and then use it when creating new classes:
```lua
local Object = class('Object')
...
local MyClass = class('MyClass', Object)
```
If you are using a library which depends on the internal implementation of middleclass (for example, [stateful.lua](https://github.com/kikito/stateful.lua) they might not work with middleclass 4.0. You might need to update those other libraries.
Middleclass 4.0 comes with support for `__index` metamethod support. If your library manipulated the classes' `__instanceDict` internal attribute, you might do the same thing now using `__index` instead.
Also note that the class method `:implements` has been removed.
Updating from 2.x to 3.x
========================
Middleclass used to expose several global variables on the main scope. It does not do that anymore.
`class` is now returned by `require 'middleclass'`, and it is not set globally. So you can do this:
```lua
local class = require 'middleclass'
local MyClass = class('MyClass') -- works as before
```
`Object` is not a global variable any more. But you can get it from `class.Object`
```lua
local class = require 'middleclass'
local Object = class.Object
print(Object) -- prints 'class Object'
```
The public functions `instanceOf`, `subclassOf` and `includes` have been replaced by `Object.isInstanceOf`, `Object.static.isSubclassOf` and `Object.static.includes`.
Prior to 3.x:
```lua
instanceOf(MyClass, obj)
subclassOf(Object, aClass)
includes(aMixin, aClass)
```
Since 3.x:
```lua
obj:isInstanceOf(MyClass)
aClass:isSubclassOf(Object)
aClass:includes(aMixin)
```
The 3.x code snippet will throw an error if `obj` is not an object, or if `aClass` is not a class (since they will not implement `isInstanceOf`, `isSubclassOf` or `includes`).
If you are unsure of whether `obj` and `aClass` are an object or a class, you can use the methods in `Object`. They are prepared to work with random types, not just classes and instances:
```lua
Object.isInstanceOf(obj, MyClass)
Object.isSubclassOf(aClass, Object)
Object.includes(aClass, aMixin)
```
Notice that the parameter order is not the same now as it was in 2.x. Also note the change in naming: `isInstanceOf` instead of `instanceOf`, and `isSubclassOf` instead of `subclassOf`.

View File

@ -1,5 +1,5 @@
local middleclass = {
_VERSION = 'middleclass v3.2.0',
_VERSION = 'middleclass v4.0.0',
_DESCRIPTION = 'Object Orientation for Lua',
_URL = 'https://github.com/kikito/middleclass',
_LICENSE = [[
@ -28,176 +28,151 @@ local middleclass = {
]]
}
local _metamethods = {}
for m in ([[ add band bor bxor bnot call concat div eq
gc ipairs idiv le len lt metatable mod mode
mul pairs pow shl shr sub tostring unm ]]):gmatch("%S+") do
_metamethods['__' .. m] = true
end
local function _setClassDictionariesMetatables(aClass)
local dict = aClass.__instanceDict
dict.__index = dict
local super = aClass.super
if super then
local superStatic = super.static
setmetatable(dict, { __index = super.__instanceDict })
setmetatable(aClass.static, { __index = function(_,k) return rawget(dict,k) or superStatic[k] end })
local function _createIndexWrapper(aClass, f)
if f == nil then
return aClass.__instanceDict
else
setmetatable(aClass.static, { __index = function(_,k) return dict[k] end })
end
end
return function(self, name)
local value = aClass.__instanceDict[name]
local function _propagateMetamethod(aClass, name, f)
for subclass in pairs(aClass.subclasses) do
if not subclass.__metamethods[name] then
subclass.__instanceDict[name] = f
_propagateMetamethod(subclass, name, f)
end
end
end
local function _updateClassDict(aClass, key, value)
if _metamethods[key] then
if value == nil then
aClass.__metamethods[key] = nil
if aClass.super then
value = aClass.super.__instanceDict[key]
if value ~= nil then
return value
elseif type(f) == "function" then
return (f(self, name))
else
return f[name]
end
else
aClass.__metamethods[key] = true
end
end
end
_propagateMetamethod(aClass, key, value)
local function _propagateInstanceMethod(aClass, name, f)
f = name == "__index" and _createIndexWrapper(aClass, f) or f
aClass.__instanceDict[name] = f
for subclass in pairs(aClass.subclasses) do
if rawget(subclass.__declaredMethods, name) == nil then
_propagateInstanceMethod(subclass, name, f)
end
end
end
local function _declareInstanceMethod(aClass, name, f)
aClass.__declaredMethods[name] = f
if f == nil and aClass.super then
f = aClass.super.__instanceDict[name]
end
aClass.__instanceDict[key] = value
_propagateInstanceMethod(aClass, name, f)
end
local function _setClassMetatable(aClass)
setmetatable(aClass, {
__tostring = function() return "class " .. aClass.name end,
__index = aClass.static,
__newindex = _updateClassDict,
__call = function(self, ...) return self:new(...) end
})
end
local function _tostring(self) return "class " .. self.name end
local function _call(self, ...) return self:new(...) end
local function _createClass(name, super)
local aClass = { name = name, super = super, static = {}, __mixins = {}, __instanceDict = {}, __metamethods = {} }
aClass.subclasses = setmetatable({}, {__mode = "k"})
local dict = {}
dict.__index = dict
_setClassDictionariesMetatables(aClass)
_setClassMetatable(aClass)
local aClass = { name = name, super = super, static = {},
__instanceDict = dict, __declaredMethods = {},
subclasses = setmetatable({}, {__mode='k'}) }
if super then
setmetatable(aClass.static, { __index = function(_,k) return rawget(dict,k) or super.static[k] end })
else
setmetatable(aClass.static, { __index = function(_,k) return rawget(dict,k) end })
end
setmetatable(aClass, { __index = aClass.static, __tostring = _tostring,
__call = _call, __newindex = _declareInstanceMethod })
return aClass
end
local function _setSubclassMetamethods(aClass, subclass)
for m in pairs(_metamethods) do
subclass.__instanceDict[m] = aClass.__instanceDict[m]
end
end
local function _setDefaultInitializeMethod(aClass, super)
aClass.initialize = function(instance, ...)
return super.initialize(instance, ...)
end
end
local function _includeMixin(aClass, mixin)
assert(type(mixin)=='table', "mixin must be a table")
assert(type(mixin) == 'table', "mixin must be a table")
for name,method in pairs(mixin) do
if name ~= "included" and name ~= "static" then aClass[name] = method end
end
if mixin.static then
for name,method in pairs(mixin.static) do
aClass.static[name] = method
end
for name,method in pairs(mixin.static or {}) do
aClass.static[name] = method
end
if type(mixin.included)=="function" then mixin:included(aClass) end
aClass.__mixins[mixin] = true
return aClass
end
local Object = _createClass("Object", nil)
local DefaultMixin = {
__tostring = function(self) return "instance of " .. tostring(self.class) end,
function Object.static:allocate()
assert(type(self) == 'table', "Make sure that you are using 'Class:allocate' instead of 'Class.allocate'")
return setmetatable({ class = self }, self.__instanceDict)
initialize = function(self, ...) end,
isInstanceOf = function(self, aClass)
return type(self) == 'table' and
type(self.class) == 'table' and
type(aClass) == 'table' and
( aClass == self.class or
type(aClass.isSubclassOf) == 'function' and
self.class:isSubclassOf(aClass) )
end,
static = {
allocate = function(self)
assert(type(self) == 'table', "Make sure that you are using 'Class:allocate' instead of 'Class.allocate'")
return setmetatable({ class = self }, self.__instanceDict)
end,
new = function(self, ...)
assert(type(self) == 'table', "Make sure that you are using 'Class:new' instead of 'Class.new'")
local instance = self:allocate()
instance:initialize(...)
return instance
end,
subclass = function(self, name)
assert(type(self) == 'table', "Make sure that you are using 'Class:subclass' instead of 'Class.subclass'")
assert(type(name) == "string", "You must provide a name(string) for your class")
local subclass = _createClass(name, self)
for name, f in pairs(self.__instanceDict) do
_propagateInstanceMethod(subclass, name, f)
end
subclass.initialize = function(instance, ...) return self.initialize(instance, ...) end
self.subclasses[subclass] = true
self:subclassed(subclass)
return subclass
end,
subclassed = function(self, other) end,
isSubclassOf = function(self, other)
return type(other) == 'table' and
type(self) == 'table' and
type(self.super) == 'table' and
( self.super == other or
type(self.super.isSubclassOf) == 'function' and
self.super:isSubclassOf(other) )
end,
include = function(self, ...)
assert(type(self) == 'table', "Make sure you that you are using 'Class:include' instead of 'Class.include'")
for _,mixin in ipairs({...}) do _includeMixin(self, mixin) end
return self
end
}
}
function middleclass.class(name, super)
assert(type(name) == 'string', "A name (string) is needed for the new class")
return super and super:subclass(name) or _includeMixin(_createClass(name), DefaultMixin)
end
function Object.static:new(...)
local instance = self:allocate()
instance:initialize(...)
return instance
end
function Object.static:subclass(name)
assert(type(self) == 'table', "Make sure that you are using 'Class:subclass' instead of 'Class.subclass'")
assert(type(name) == "string", "You must provide a name(string) for your class")
local subclass = _createClass(name, self)
_setSubclassMetamethods(self, subclass)
_setDefaultInitializeMethod(subclass, self)
self.subclasses[subclass] = true
self:subclassed(subclass)
return subclass
end
function Object.static:subclassed(other) end
function Object.static:isSubclassOf(other)
return type(other) == 'table' and
type(self) == 'table' and
type(self.super) == 'table' and
( self.super == other or
type(self.super.isSubclassOf) == 'function' and
self.super:isSubclassOf(other)
)
end
function Object.static:include( ... )
assert(type(self) == 'table', "Make sure you that you are using 'Class:include' instead of 'Class.include'")
for _,mixin in ipairs({...}) do _includeMixin(self, mixin) end
return self
end
function Object.static:includes(mixin)
return type(mixin) == 'table' and
type(self) == 'table' and
type(self.__mixins) == 'table' and
( self.__mixins[mixin] or
type(self.super) == 'table' and
type(self.super.includes) == 'function' and
self.super:includes(mixin)
)
end
function Object:initialize() end
function Object:__tostring() return "instance of " .. tostring(self.class) end
function Object:isInstanceOf(aClass)
return type(self) == 'table' and
type(self.class) == 'table' and
type(aClass) == 'table' and
( aClass == self.class or
type(aClass.isSubclassOf) == 'function' and
self.class:isSubclassOf(aClass)
)
end
function middleclass.class(name, super, ...)
super = super or Object
return super:subclass(name, ...)
end
middleclass.Object = Object
setmetatable(middleclass, { __call = function(_, ...) return middleclass.class(...) end })
return middleclass

View File

@ -13,7 +13,7 @@ describe('class()', function()
it('the resulting class has the correct name and Object as its superclass', function()
local TheClass = class('TheClass')
assert.equal(TheClass.name, 'TheClass')
assert.equal(TheClass.super, Object)
assert.is_nil(TheClass.super)
end)
end)

View File

@ -1,8 +1,10 @@
local class = require 'middleclass'
local Object = class.Object
describe('Object', function()
describe('Default methods', function()
local Object
before_each(function()
Object = class('Object')
end)
describe('name', function()
it('is correctly set', function()
@ -95,12 +97,11 @@ describe('Object', function()
local allocated = SubClass:allocate()
assert.is_true(allocated.mark)
end)
end)
describe('new', function()
describe('new', function()
it('initializes instances properly', function()
local instance = SubClass:new()
@ -163,12 +164,6 @@ describe('Object', function()
local o1, o2, o3 = Class1:new(), Class2:new(), Class3:new()
it('isInstanceOf(Object)', function()
assert.is_true(o1:isInstanceOf(Object))
assert.is_true(o2:isInstanceOf(Object))
assert.is_true(o3:isInstanceOf(Object))
end)
it('isInstanceOf its class', function()
assert.is_true(o1:isInstanceOf(Class1))
assert.is_true(o2:isInstanceOf(Class2))
@ -241,12 +236,6 @@ describe('Object', function()
local Class3 = class('Class3', Class2)
local UnrelatedClass = class('Unrelated')
it('isSubclassOf(Object)', function()
assert.is_true(Class1:isSubclassOf(Object))
assert.is_true(Class2:isSubclassOf(Object))
assert.is_true(Class3:isSubclassOf(Object))
end)
it('is subclassOf its direct superclass', function()
assert.is_true(Class2:isSubclassOf(Class1))
assert.is_true(Class3:isSubclassOf(Class2))
@ -270,73 +259,6 @@ describe('Object', function()
end)
end)
describe('includes', function()
describe('nils, numbers, etc', function()
local o = Object:new()
local primitives = {nil, 1, 'hello', {}, function() end}
for _,primitive in pairs(primitives) do
local theType = type(primitive)
describe('A ' .. theType, function()
local f1 = function() return Object.includes(Object, primitive) end
local f2 = function() return Object.includes(primitive, o) end
local f3 = function() return Object.includes(primitive, primitive) end
describe('don\'t throw errors', function()
it('includes(Object, '.. theType ..')', function()
assert.not_error(f1)
end)
it('includes(' .. theType .. ', Object:new())', function()
assert.not_error(f2)
end)
it('includes(' .. theType .. ',' .. theType ..')', function()
assert.not_error(f3)
end)
end)
it('make includes return false', function()
assert.is_false(f1())
assert.is_false(f2())
assert.is_false(f3())
end)
end)
end -- for
end)
describe('A class', function()
local Class1 = class('Class1')
local Class2 = class('Class2', Class1)
local Class3 = class('Class3', Class2)
local UnrelatedClass = class('Unrelated')
local hasFoo = { foo=function() return 'foo' end }
Class1:include(hasFoo)
it('returns true if it includes a mixin', function()
assert.is_true(Class1:includes(hasFoo))
end)
it('returns true if its superclass includes a mixin', function()
assert.is_true(Class2:includes(hasFoo))
assert.is_true(Class3:includes(hasFoo))
end)
it('returns false otherwise', function()
assert.is_false(UnrelatedClass:includes(hasFoo))
end)
end)
end)
end)

View File

@ -204,6 +204,92 @@ describe('Metamethods', function()
end)
end)
describe('Custom __index and __newindex', function()
describe('Tables', function()
local Proxy, fallback, p
before_each(function()
Proxy = class('Proxy')
fallback = {foo = 'bar', common = 'fallback'}
Proxy.__index = fallback
Proxy.__newindex = fallback
Proxy.common = 'class'
p = Proxy()
end)
it('uses __index', function()
assert.equal(p.foo, 'bar')
end)
it('does not use __index when field exists in class', function()
assert.equal(p.common, 'class')
end)
it('uses __newindex', function()
p.key = 'value'
assert.equal(fallback.key, 'value')
end)
it('uses __newindex when field exists in class', function()
p.common = 'value'
assert.equal(p.common, 'class')
assert.equal(Proxy.common, 'class')
assert.equal(fallback.common, 'value')
end)
end)
describe('Functions', function()
local Namespace, Rectangle, r
before_each(function()
Namespace = class('Namespace')
function Namespace:__index(name)
local getter = self.class[name.."Getter"]
if getter then return getter(self) end
end
function Namespace:__newindex(name, value)
local setter = self.class[name.."Setter"]
if setter then setter(self, value) else rawset(self, name, value) end
end
Rectangle = class('Rectangle', Namespace)
function Rectangle:initialize(x, y, scale)
self._scale, self.x, self.y = 1, x, y
self.scale = scale
end
function Rectangle:scaleGetter() return self._scale end
function Rectangle:scaleSetter(v)
self.x = self.x*v/self._scale
self.y = self.y*v/self._scale
self._scale = v
end
function Rectangle:areaGetter() return self.x * self.y end
r = Rectangle(3, 4, 2)
end)
it('uses setter', function()
assert.equal(r.x, 6)
assert.equal(r.y, 8)
r.scale = 3
assert.equal(r.x, 9)
assert.equal(r.y, 12)
end)
it('uses getters', function()
assert.equal(r.scale, 2)
assert.equal(r.area, 48)
end)
it('updates inherited __index', function()
function Namespace.__index() return 42 end
assert.equal(r.area, 42)
function Rectangle.__index() return 24 end
assert.equal(r.area, 24)
function Namespace.__index() return 96 end
assert.equal(r.area, 24)
Rectangle.__index = nil
assert.equal(r.area, 96)
end)
end)
end)
describe('Default Metamethods', function()
local Peter, peter